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
package/scripts/fetch.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// The fetch stage, as fetch-alpine.sh, fetch-bun.sh and scripts/fetch.sh do
|
|
2
|
+
// it: download the pinned Alpine packages and Bun release, verify their
|
|
3
|
+
// SHA-256, and lay out kernel/<arch>/ and native/<arch>/.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/fetch.js both parts; BUNINU_ARCH, LINUX_FLAVOR,
|
|
6
|
+
// REAL_MACHINE and ALPINE_MIRROR as in arch.js
|
|
7
|
+
//
|
|
8
|
+
// Unlike fetch-alpine.sh, the release's module directory is emptied first,
|
|
9
|
+
// so modules from an earlier --real fetch do not stay behind; and a download
|
|
10
|
+
// lands under its final name only once its hash matched.
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import {
|
|
14
|
+
chmodSync,
|
|
15
|
+
copyFileSync,
|
|
16
|
+
createReadStream,
|
|
17
|
+
createWriteStream,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
statSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { Readable } from "node:stream";
|
|
30
|
+
import { pipeline } from "node:stream/promises";
|
|
31
|
+
import { gunzipSync } from "node:zlib";
|
|
32
|
+
import { config, rootDir, runIfMain, withUmask022 } from "./arch.js";
|
|
33
|
+
import { filterAliases, select, trimDependencies } from "./modules.js";
|
|
34
|
+
|
|
35
|
+
const sha256File = async (path) => {
|
|
36
|
+
const hash = createHash("sha256");
|
|
37
|
+
await pipeline(createReadStream(path), hash);
|
|
38
|
+
return hash.digest("hex");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Downloads `url` to downloadsDir/name unless a file with that SHA-256 is
|
|
43
|
+
* already there. The hash is both the cache key and the integrity check.
|
|
44
|
+
*/
|
|
45
|
+
export const download = async (cfg, url, name, sha256) => {
|
|
46
|
+
const path = join(cfg.downloadsDir, name);
|
|
47
|
+
if (existsSync(path) && (await sha256File(path)) === sha256) {
|
|
48
|
+
console.log(`cached ${name}`);
|
|
49
|
+
return path;
|
|
50
|
+
}
|
|
51
|
+
console.log(`fetch ${name}`);
|
|
52
|
+
mkdirSync(cfg.downloadsDir, { recursive: true });
|
|
53
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
54
|
+
if (!response.ok || !response.body) throw new Error(`${url}: HTTP ${response.status}`);
|
|
55
|
+
const partial = `${path}.part`;
|
|
56
|
+
const hash = createHash("sha256");
|
|
57
|
+
const hashing = new TransformStream({ transform(chunk, controller) { hash.update(chunk); controller.enqueue(chunk); } });
|
|
58
|
+
await pipeline(Readable.fromWeb(response.body.pipeThrough(hashing)), createWriteStream(partial));
|
|
59
|
+
const actual = hash.digest("hex");
|
|
60
|
+
if (actual !== sha256) {
|
|
61
|
+
rmSync(partial, { force: true });
|
|
62
|
+
throw new Error(`${name}: SHA-256 ${actual}, expected ${sha256}`);
|
|
63
|
+
}
|
|
64
|
+
renameSync(partial, path);
|
|
65
|
+
return path;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The regular files of a tar archive (gzip members concatenated, as an .apk
|
|
70
|
+
* is), by name. pax ("x") and GNU long-name ("L") headers give names longer
|
|
71
|
+
* than the ustar fields.
|
|
72
|
+
*/
|
|
73
|
+
export const readTar = (path) => {
|
|
74
|
+
const data = gunzipSync(readFileSync(path));
|
|
75
|
+
const files = new Map();
|
|
76
|
+
let longName = null;
|
|
77
|
+
for (let offset = 0; offset + 512 <= data.length;) {
|
|
78
|
+
const header = data.subarray(offset, offset + 512);
|
|
79
|
+
if (header.every((byte) => byte === 0)) { offset += 512; continue; }
|
|
80
|
+
const field = (start, length) => header.toString("utf8", start, start + length).replace(/\0.*$/s, "");
|
|
81
|
+
const size = Number.parseInt(field(124, 12).trim() || "0", 8);
|
|
82
|
+
const type = String.fromCharCode(header[156] || 0x30);
|
|
83
|
+
const body = data.subarray(offset + 512, offset + 512 + size);
|
|
84
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
85
|
+
if (type === "x") {
|
|
86
|
+
// pax records: "LENGTH key=value\n"
|
|
87
|
+
for (const record of body.toString("utf8").split("\n")) {
|
|
88
|
+
const match = /^\d+ path=(.*)$/.exec(record);
|
|
89
|
+
if (match) longName = match[1];
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (type === "L") { longName = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
|
|
94
|
+
if (type === "g") continue;
|
|
95
|
+
const prefix = field(345, 155);
|
|
96
|
+
const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
|
|
97
|
+
longName = null;
|
|
98
|
+
if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
get(name) {
|
|
102
|
+
const bytes = files.get(name);
|
|
103
|
+
if (!bytes) throw new Error(`${path}: no ${name}`);
|
|
104
|
+
return bytes;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const writeTo = (path, bytes) => {
|
|
110
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
111
|
+
writeFileSync(path, bytes);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Both run with umask 022 (see arch.js withUmask022): native/ and kernel/
|
|
115
|
+
// are packed with their modes.
|
|
116
|
+
|
|
117
|
+
/** fetch-alpine.sh: the kernel, its selected modules, musl's loader and the EFI stub. */
|
|
118
|
+
export const fetchAlpine = (cfg = config()) => withUmask022(async () => {
|
|
119
|
+
mkdirSync(cfg.kernelDir, { recursive: true });
|
|
120
|
+
mkdirSync(join(cfg.nativeDir, "lib"), { recursive: true });
|
|
121
|
+
const linux = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.linuxPackage}`, cfg.linuxPackage, cfg.linuxSha256));
|
|
122
|
+
const musl = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.muslPackage}`, cfg.muslPackage, cfg.muslSha256));
|
|
123
|
+
const stub = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.stubPackage}`, cfg.stubPackage, cfg.stubSha256));
|
|
124
|
+
|
|
125
|
+
// On aarch64 vmlinuz is an EFI zboot image, started by the stub like the x86_64 bzImage.
|
|
126
|
+
writeTo(cfg.kernelImage, linux.get(`boot/vmlinuz-${cfg.flavor}`));
|
|
127
|
+
writeTo(join(cfg.nativeDir, "lib", cfg.muslLoader), musl.get(`lib/${cfg.muslLoader}`));
|
|
128
|
+
writeTo(join(cfg.kernelDir, cfg.efiStub), stub.get(`usr/lib/systemd/boot/efi/${cfg.efiStub}`));
|
|
129
|
+
|
|
130
|
+
const release = `lib/modules/${cfg.kernelRelease}`;
|
|
131
|
+
const dep = linux.get(`${release}/modules.dep`).toString();
|
|
132
|
+
const builtin = linux.get(`${release}/modules.builtin`);
|
|
133
|
+
const { selected, notes } = select({ dep, builtin: builtin.toString(), packageName: cfg.linuxPackage, arch: cfg.arch, flavor: cfg.flavor, real: cfg.real });
|
|
134
|
+
for (const note of notes) console.error(note);
|
|
135
|
+
rmSync(cfg.modulesRoot, { recursive: true, force: true });
|
|
136
|
+
writeTo(join(cfg.modulesRoot, "modules.builtin"), builtin);
|
|
137
|
+
for (const path of selected) {
|
|
138
|
+
writeTo(join(cfg.modulesRoot, path.replace(/\.gz$/, "")), gunzipSync(linux.get(`${release}/${path}`)));
|
|
139
|
+
}
|
|
140
|
+
writeTo(join(cfg.modulesRoot, "modules.dep"), trimDependencies(dep, selected));
|
|
141
|
+
writeTo(join(cfg.modulesRoot, "modules.alias"), filterAliases(linux.get(`${release}/modules.alias`).toString(), selected));
|
|
142
|
+
|
|
143
|
+
chmodSync(join(cfg.nativeDir, "lib", cfg.muslLoader), 0o755);
|
|
144
|
+
console.log(`Fetched Alpine ${cfg.arch} linux-${cfg.flavor} kernel, ${selected.length} modules, musl, and the ${cfg.efiStub} UKI stub.`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** fetch-bun.sh: Bun from its release zip, and the GCC runtime it links against. */
|
|
148
|
+
export const fetchBun = (cfg = config()) => withUmask022(async () => {
|
|
149
|
+
for (const directory of [
|
|
150
|
+
cfg.downloadsDir,
|
|
151
|
+
join(cfg.nativeDir, "bin"),
|
|
152
|
+
join(cfg.nativeDir, "lib"),
|
|
153
|
+
join(rootDir, "initramfs/dev"),
|
|
154
|
+
join(rootDir, "initramfs/proc"),
|
|
155
|
+
join(rootDir, "initramfs/sys"),
|
|
156
|
+
join(rootDir, "initramfs/tmp"),
|
|
157
|
+
]) {
|
|
158
|
+
mkdirSync(directory, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
const zip = await download(cfg, cfg.bunUrl, cfg.bunZip, cfg.bunSha256);
|
|
161
|
+
const libstdcpp = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.libstdcppPackage}`, cfg.libstdcppPackage, cfg.libstdcppSha256));
|
|
162
|
+
const libgcc = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.libgccPackage}`, cfg.libgccPackage, cfg.libgccSha256));
|
|
163
|
+
const loader = join(cfg.nativeDir, "lib", cfg.muslLoader);
|
|
164
|
+
if (!existsSync(loader) || statSync(loader).size === 0) {
|
|
165
|
+
throw new Error("fetch stage incomplete: run fetchAlpine first (node scripts/fetch.js runs both)");
|
|
166
|
+
}
|
|
167
|
+
// initramfs/bin/unzip: the same reader the guest has.
|
|
168
|
+
const unzip = spawnSync(process.execPath, [
|
|
169
|
+
join(rootDir, "initramfs/bin/unzip"),
|
|
170
|
+
"-p",
|
|
171
|
+
zip,
|
|
172
|
+
`${cfg.bunBuild}/bun`,
|
|
173
|
+
], { maxBuffer: 1 << 30 });
|
|
174
|
+
if (unzip.status !== 0) throw new Error(`unzip ${zip}: ${unzip.stderr}`);
|
|
175
|
+
writeTo(join(cfg.nativeDir, "bin", "bun"), unzip.stdout);
|
|
176
|
+
writeTo(join(cfg.nativeDir, "lib", "libstdc++.so.6"), libstdcpp.get("usr/lib/libstdc++.so.6.0.34"));
|
|
177
|
+
writeTo(join(cfg.nativeDir, "lib", "libgcc_s.so.1"), libgcc.get("usr/lib/libgcc_s.so.1"));
|
|
178
|
+
// libc as a distinct inode from the active dynamic loader: dlopen on the
|
|
179
|
+
// loader inode itself deadlocks, while this physical copy is FFI-safe.
|
|
180
|
+
copyFileSync(loader, join(cfg.nativeDir, "lib", cfg.muslLibc));
|
|
181
|
+
chmodSync(join(rootDir, "initramfs/init.js"), 0o755);
|
|
182
|
+
chmodSync(join(cfg.nativeDir, "bin", "bun"), 0o755);
|
|
183
|
+
for (const name of readdirSync(join(cfg.nativeDir, "lib"))) {
|
|
184
|
+
if (/\.so/.test(name)) chmodSync(join(cfg.nativeDir, "lib", name), 0o755);
|
|
185
|
+
}
|
|
186
|
+
console.log(`Fetched Bun ${cfg.bunVersion} and its ${cfg.arch} musl runtime into ${cfg.nativeDir}/.`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export const fetchAll = async (cfg = config()) => {
|
|
190
|
+
await fetchAlpine(cfg);
|
|
191
|
+
await fetchBun(cfg);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
runIfMain(import.meta.url, () => fetchAll());
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// The kernel modules the initramfs ships, as fetch-alpine.sh picks them: each
|
|
2
|
+
// requested by name, with what the package's modules.dep says it needs. The
|
|
3
|
+
// image gets a modules.dep trimmed to the shipped set, the fs-* lines and
|
|
4
|
+
// the device ids of the shipped modules from modules.alias, and the
|
|
5
|
+
// package's modules.builtin; /lib/modprobe.js reads those three files.
|
|
6
|
+
//
|
|
7
|
+
// fetch.js uses select(), trimDependencies() and filterAliases().
|
|
8
|
+
|
|
9
|
+
// Network: virtio-net for QEMU (init.js loads these at boot). virtio_pci is
|
|
10
|
+
// the PCI transport for every virtio device; linux-virt builds it in,
|
|
11
|
+
// linux-lts has it as a module that nothing in modules.dep pulls in.
|
|
12
|
+
// Storage: what /bin/mount loads on demand. Pointing devices for
|
|
13
|
+
// `bunterm --mouse`. The framebuffer for bunterm on lts. Sound for QEMU's and
|
|
14
|
+
// PCs' usual devices. --real adds the USB keyboard, PC storage and ACPI
|
|
15
|
+
// drivers, and the wired and USB network drivers.
|
|
16
|
+
const required = (names) => names.map((name) => ({ name }));
|
|
17
|
+
const optional = (names) => names.map((name) => ({ name, optional: true }));
|
|
18
|
+
|
|
19
|
+
export const requests = ({ arch, flavor, real }) => [
|
|
20
|
+
...required([
|
|
21
|
+
"failover",
|
|
22
|
+
"net_failover",
|
|
23
|
+
"virtio_net",
|
|
24
|
+
]),
|
|
25
|
+
...optional([
|
|
26
|
+
"virtio_pci",
|
|
27
|
+
]),
|
|
28
|
+
...required([
|
|
29
|
+
"virtio_blk",
|
|
30
|
+
"ext4",
|
|
31
|
+
"vfat",
|
|
32
|
+
"exfat",
|
|
33
|
+
"ntfs3",
|
|
34
|
+
"nls_utf8",
|
|
35
|
+
"nls_cp437",
|
|
36
|
+
"nls_iso8859-1",
|
|
37
|
+
]),
|
|
38
|
+
...optional([
|
|
39
|
+
"ext2",
|
|
40
|
+
]),
|
|
41
|
+
...required([
|
|
42
|
+
"evdev",
|
|
43
|
+
arch === "x86_64" ? "psmouse" : "virtio_input",
|
|
44
|
+
]),
|
|
45
|
+
...(flavor === "lts" ? optional([
|
|
46
|
+
"simpledrm",
|
|
47
|
+
]) : []),
|
|
48
|
+
...optional([
|
|
49
|
+
"snd-ens1371",
|
|
50
|
+
"snd-hda-intel",
|
|
51
|
+
"snd-hda-codec-generic",
|
|
52
|
+
"snd-hda-codec-hdmi",
|
|
53
|
+
"snd-usb-audio",
|
|
54
|
+
]),
|
|
55
|
+
...(real ? required([
|
|
56
|
+
// USB keyboard
|
|
57
|
+
"usb-common",
|
|
58
|
+
"usbcore",
|
|
59
|
+
"xhci-hcd",
|
|
60
|
+
"xhci-pci",
|
|
61
|
+
"xhci-pci-renesas",
|
|
62
|
+
"hid",
|
|
63
|
+
"hid-generic",
|
|
64
|
+
"usbhid",
|
|
65
|
+
// SATA/AHCI, PATA, NVMe (with Intel VMD), USB mass storage and UAS
|
|
66
|
+
"sd_mod",
|
|
67
|
+
"ahci",
|
|
68
|
+
"ata_generic",
|
|
69
|
+
"pata_acpi",
|
|
70
|
+
"nvme",
|
|
71
|
+
"vmd",
|
|
72
|
+
"usb-storage",
|
|
73
|
+
"uas",
|
|
74
|
+
// ACPI: battery, AC adapter, power button, thermal zones
|
|
75
|
+
"battery",
|
|
76
|
+
"ac",
|
|
77
|
+
"button",
|
|
78
|
+
"thermal",
|
|
79
|
+
// Wired NICs: Intel, Realtek (with its PHY), Qualcomm Atheros, Broadcom
|
|
80
|
+
"e1000",
|
|
81
|
+
"e1000e",
|
|
82
|
+
"igb",
|
|
83
|
+
"igc",
|
|
84
|
+
"r8169",
|
|
85
|
+
"realtek",
|
|
86
|
+
"alx",
|
|
87
|
+
"tg3",
|
|
88
|
+
// USB NICs and Android tethering (RNDIS, CDC ECM/NCM/EEM, subset, zaurus)
|
|
89
|
+
"r8152",
|
|
90
|
+
"ax88179_178a",
|
|
91
|
+
"cdc_ether",
|
|
92
|
+
"rndis_host",
|
|
93
|
+
"cdc_ncm",
|
|
94
|
+
"cdc_eem",
|
|
95
|
+
"cdc_subset",
|
|
96
|
+
"zaurus",
|
|
97
|
+
]) : []),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The modules.dep paths (".ko.gz", relative to the release directory) to
|
|
102
|
+
* ship, dependencies after the module that pulled them in, and notes about
|
|
103
|
+
* optional modules the kernel neither builds in nor packages.
|
|
104
|
+
*/
|
|
105
|
+
export const select = ({ dep, builtin, packageName, ...target }) => {
|
|
106
|
+
const lines = dep.split("\n");
|
|
107
|
+
const builtinLines = builtin.split("\n");
|
|
108
|
+
const selected = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
const notes = [];
|
|
111
|
+
const find = (name) => lines.find((line) => line.includes(`/${name}.ko.gz:`));
|
|
112
|
+
const add = (name) => {
|
|
113
|
+
// An explicit stack instead of recursion; the order matches the shell's
|
|
114
|
+
// depth-first walk.
|
|
115
|
+
const stack = [name];
|
|
116
|
+
while (stack.length) {
|
|
117
|
+
const current = stack.pop();
|
|
118
|
+
const line = find(current);
|
|
119
|
+
if (!line) throw new Error(`module ${current} is not in ${packageName}`);
|
|
120
|
+
const path = line.slice(0, line.indexOf(":"));
|
|
121
|
+
if (seen.has(path)) continue;
|
|
122
|
+
seen.add(path);
|
|
123
|
+
selected.push(path);
|
|
124
|
+
const dependencies = line.slice(line.indexOf(":") + 1).trim().split(/\s+/).filter(Boolean)
|
|
125
|
+
.map((dependency) => dependency.slice(dependency.lastIndexOf("/") + 1).replace(/\.ko\.gz$/, ""));
|
|
126
|
+
stack.push(...dependencies.reverse());
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
for (const { name, optional } of requests(target)) {
|
|
130
|
+
if (!optional || find(name)) add(name);
|
|
131
|
+
else if (!builtinLines.some((line) => line.endsWith(`/${name}.ko`))) {
|
|
132
|
+
notes.push(`note: module ${name} is not in ${target.arch} ${packageName}; skipped`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { selected, notes };
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** modules.dep with only the shipped modules' lines, .ko.gz renamed .ko. */
|
|
139
|
+
export const trimDependencies = (dep, selected) => {
|
|
140
|
+
const wanted = new Set(selected);
|
|
141
|
+
return dep.split("\n")
|
|
142
|
+
.filter((line) => line.includes(":") && wanted.has(line.slice(0, line.indexOf(":"))))
|
|
143
|
+
.map((line) => `${line.replaceAll(".ko.gz", ".ko")}\n`)
|
|
144
|
+
.join("");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** modules.alias: the fs-* names, and the pci:/usb:/virtio: ids of shipped modules. */
|
|
148
|
+
export const filterAliases = (alias, selected) => {
|
|
149
|
+
const shipped = new Set(selected.map((path) => path.slice(path.lastIndexOf("/") + 1).replace(/\.ko\.gz$/, "").replaceAll("-", "_")));
|
|
150
|
+
return alias.split("\n")
|
|
151
|
+
.filter((line) => {
|
|
152
|
+
const fields = line.trim().split(/\s+/);
|
|
153
|
+
if (/^fs-/.test(fields[1] ?? "")) return true;
|
|
154
|
+
return /^(pci|usb|virtio):/.test(fields[1] ?? "") && shipped.has((fields[2] ?? "").replaceAll("-", "_"));
|
|
155
|
+
})
|
|
156
|
+
.map((line) => `${line}\n`)
|
|
157
|
+
.join("");
|
|
158
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
+
# What this script creates is packed with its modes: the same umask as
|
|
4
|
+
# PRoot and most hosts, not native Termux's 0077.
|
|
5
|
+
umask 022
|
|
3
6
|
|
|
4
7
|
cd "$(dirname "$0")/.."
|
|
5
8
|
|
|
@@ -22,6 +25,12 @@ trap 'rm -rf "$staging"; rm -f "$archive"' EXIT
|
|
|
22
25
|
cp -a --reflink=auto initramfs/. "$staging/"
|
|
23
26
|
rm -rf "$staging/bin/bun" "$staging/lib/modules"
|
|
24
27
|
cp -a --reflink=auto "$native_dir/." "$staging/"
|
|
28
|
+
# Only this kernel's modules: native/<arch>/ keeps every release fetched so
|
|
29
|
+
# far (an older pin, the other flavor), and none of those can load.
|
|
30
|
+
for release_dir in "$staging/lib/modules"/*; do
|
|
31
|
+
[ -e "$release_dir" ] || continue
|
|
32
|
+
[ "${release_dir##*/}" = "$kernel_release" ] || rm -rf "$release_dir"
|
|
33
|
+
done
|
|
25
34
|
# Keep the project README canonical while making it available to
|
|
26
35
|
# buninu-linux-help from the conventional installed-document path.
|
|
27
36
|
cp README.md "$staging/usr/share/doc/buninu-linux/README.md"
|
|
@@ -50,6 +59,14 @@ mkdir -p "$staging/usr/share/icons/hicolor/512x512/apps" "$staging/usr/share/pix
|
|
|
50
59
|
ln -sfn ../../../../../../buninu/icon.png "$staging/usr/share/icons/hicolor/512x512/apps/buninu-linux.png"
|
|
51
60
|
ln -sfn ../../../buninu/icon.png "$staging/usr/share/pixmaps/buninu-linux.png"
|
|
52
61
|
|
|
62
|
+
# The modes git would check out with umask 022: directories 755, files 755
|
|
63
|
+
# when their owner may execute them and 644 otherwise (symlinks untouched).
|
|
64
|
+
# A checkout made under another umask (native Termux's 0077 gives 600/700)
|
|
65
|
+
# then packs the same archive.
|
|
66
|
+
find "$staging" -type d -exec chmod 755 {} +
|
|
67
|
+
find "$staging" -type f -perm -100 -exec chmod 755 {} +
|
|
68
|
+
find "$staging" -type f ! -perm -100 -exec chmod 644 {} +
|
|
69
|
+
|
|
53
70
|
# BUNINU_JS_CPIO=1 writes the archive with scripts/cpio.js (bun or node)
|
|
54
71
|
# instead of fakeroot, cpio and gzip: the device nodes below exist only in
|
|
55
72
|
# the archive, so nothing needs root.
|
package/scripts/pack.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// The initramfs, as scripts/pack-initramfs.sh packs it with BUNINU_JS_CPIO=1:
|
|
2
|
+
// initramfs/ with native/<arch>/ laid over it, the README, licenses and
|
|
3
|
+
// os-release added, written by scripts/cpio.js to build/initramfs.cpio.gz.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/pack.js BUNINU_ARCH as in arch.js; SOURCE_DATE_EPOCH
|
|
6
|
+
// fixes every mtime, as cpio.js does
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
lstatSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
mkdtempSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readlinkSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
statSync,
|
|
21
|
+
symlinkSync,
|
|
22
|
+
utimesSync,
|
|
23
|
+
lutimesSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { config, jsRuntime, rootDir, runIfMain } from "./arch.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `cp -a SOURCE/. TARGET/`: modes, times and symlinks kept, directories
|
|
31
|
+
* merged into ones already there (and given the source's mode, as cp -a does).
|
|
32
|
+
*/
|
|
33
|
+
export const copyTree = (source, target) => {
|
|
34
|
+
const stat = lstatSync(source);
|
|
35
|
+
if (stat.isSymbolicLink()) {
|
|
36
|
+
rmSync(target, { force: true });
|
|
37
|
+
symlinkSync(readlinkSync(source), target);
|
|
38
|
+
lutimesSync(target, stat.atime, stat.mtime);
|
|
39
|
+
} else if (stat.isDirectory()) {
|
|
40
|
+
mkdirSync(target, { recursive: true });
|
|
41
|
+
for (const name of readdirSync(source)) copyTree(join(source, name), join(target, name));
|
|
42
|
+
chmodSync(target, stat.mode & 0o7777);
|
|
43
|
+
utimesSync(target, stat.atime, stat.mtime);
|
|
44
|
+
} else if (stat.isFile()) {
|
|
45
|
+
rmSync(target, { force: true });
|
|
46
|
+
copyFileSync(source, target);
|
|
47
|
+
chmodSync(target, stat.mode & 0o7777);
|
|
48
|
+
utimesSync(target, stat.atime, stat.mtime);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`${source}: not a file, directory or symlink`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The modes git would check out with umask 022: directories 755, files 755
|
|
56
|
+
* when their owner may execute them and 644 otherwise; symlinks untouched.
|
|
57
|
+
* A checkout made under another umask (native Termux's 0077 gives 600/700)
|
|
58
|
+
* then packs the same archive.
|
|
59
|
+
*/
|
|
60
|
+
export const normalizeModes = (path) => {
|
|
61
|
+
const stat = lstatSync(path);
|
|
62
|
+
if (stat.isSymbolicLink()) return;
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
chmodSync(path, 0o755);
|
|
65
|
+
for (const name of readdirSync(path)) normalizeModes(join(path, name));
|
|
66
|
+
} else if (stat.isFile()) {
|
|
67
|
+
chmodSync(path, stat.mode & 0o100 ? 0o755 : 0o644);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** `cp FILE TARGET`: contents and mode, a new mtime. */
|
|
72
|
+
const copyFile = (source, target) => {
|
|
73
|
+
rmSync(target, { force: true });
|
|
74
|
+
copyFileSync(source, target);
|
|
75
|
+
chmodSync(target, statSync(source).mode & 0o777 & ~process.umask());
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** `ln -sfn TARGET LINK` */
|
|
79
|
+
const link = (target, path) => {
|
|
80
|
+
rmSync(path, { force: true });
|
|
81
|
+
symlinkSync(target, path);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The device nodes of the archive, PATH:c:MAJOR:MINOR:MODE. Nothing has
|
|
86
|
+
* mounted devtmpfs when the kernel execs Bun as PID 1: console is init's fd
|
|
87
|
+
* 0/1/2 and JSC aborts without urandom; the rest keep /dev usable if the
|
|
88
|
+
* devtmpfs mount in init.js fails. The serial port is ttyS0 (4, 64) on
|
|
89
|
+
* x86_64 and the PL011 ttyAMA0 (204, 64) on aarch64.
|
|
90
|
+
*/
|
|
91
|
+
export const devices = (cfg) => [
|
|
92
|
+
"dev/console:c:5:1:0600",
|
|
93
|
+
`dev/${cfg.serialConsole}:c:${cfg.serialMajor}:64:0620`,
|
|
94
|
+
"dev/null:c:1:3:0666",
|
|
95
|
+
"dev/zero:c:1:5:0666",
|
|
96
|
+
"dev/random:c:1:8:0666",
|
|
97
|
+
"dev/urandom:c:1:9:0666",
|
|
98
|
+
"dev/tty:c:5:0:0666",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
export const osRelease = (version) => [
|
|
102
|
+
'NAME="Buninu Linux"',
|
|
103
|
+
"ID=buninu-linux",
|
|
104
|
+
`VERSION="${version}"`,
|
|
105
|
+
`VERSION_ID=${version}`,
|
|
106
|
+
`PRETTY_NAME="Buninu Linux ${version}"`,
|
|
107
|
+
'HOME_URL="https://buninu.org"',
|
|
108
|
+
'BUG_REPORT_URL="https://github.com/jjtseng93/buninu-linux/issues"',
|
|
109
|
+
"LOGO=buninu-linux",
|
|
110
|
+
"DEFAULT_HOSTNAME=buninu",
|
|
111
|
+
].map((line) => `${line}\n`).join("");
|
|
112
|
+
|
|
113
|
+
export const pack = (cfg = config()) => {
|
|
114
|
+
// The directories and files made here are packed with their modes; native
|
|
115
|
+
// Termux's umask 0077 would make /usr/lib and /usr/lib/os-release private.
|
|
116
|
+
const previousUmask = process.umask(0o022);
|
|
117
|
+
try {
|
|
118
|
+
packStaging(cfg);
|
|
119
|
+
} finally {
|
|
120
|
+
process.umask(previousUmask);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const packStaging = (cfg) => {
|
|
125
|
+
const bun = join(cfg.nativeDir, "bin", "bun");
|
|
126
|
+
if (!existsSync(bun) || !(statSync(bun).mode & 0o111)) {
|
|
127
|
+
throw new Error(`no ${bun} yet; run the fetch stage first (node scripts/fetch.js)`);
|
|
128
|
+
}
|
|
129
|
+
const build = join(rootDir, "build");
|
|
130
|
+
mkdirSync(build, { recursive: true });
|
|
131
|
+
const staging = mkdtempSync(join(rootDir, ".initramfs."));
|
|
132
|
+
try {
|
|
133
|
+
// initramfs/ is the same for every architecture; native/<arch>/ adds bun,
|
|
134
|
+
// the musl loader, the GCC runtime and the kernel modules on top of it.
|
|
135
|
+
// A checkout fetched before native/ existed still has an x86_64 bun and
|
|
136
|
+
// its modules under initramfs/; they are dropped rather than packed.
|
|
137
|
+
copyTree(join(rootDir, "initramfs"), staging);
|
|
138
|
+
rmSync(join(staging, "bin", "bun"), { force: true });
|
|
139
|
+
rmSync(join(staging, "lib", "modules"), { recursive: true, force: true });
|
|
140
|
+
copyTree(cfg.nativeDir, staging);
|
|
141
|
+
// Only this kernel's modules: native/<arch>/ keeps every release fetched
|
|
142
|
+
// so far (an older pin, the other flavor), and none of those can load.
|
|
143
|
+
const modules = join(staging, "lib", "modules");
|
|
144
|
+
for (const release of existsSync(modules) ? readdirSync(modules) : []) {
|
|
145
|
+
if (release !== cfg.kernelRelease) rmSync(join(modules, release), { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
copyFile(join(rootDir, "README.md"), join(staging, "usr/share/doc/buninu-linux/README.md"));
|
|
148
|
+
for (const name of [
|
|
149
|
+
"LICENSE",
|
|
150
|
+
"NOTICE.md",
|
|
151
|
+
"LICENSES",
|
|
152
|
+
]) copyTree(join(rootDir, name), join(staging, "usr/share/licenses", name));
|
|
153
|
+
|
|
154
|
+
// os-release(5): the canonical file in /usr/lib, /etc/os-release a link
|
|
155
|
+
// to it; build.js embeds the same file as the UKI's .osrel section.
|
|
156
|
+
const { version } = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"));
|
|
157
|
+
if (!version) throw new Error("no version in package.json");
|
|
158
|
+
writeFileSync(join(build, "os-release"), osRelease(version));
|
|
159
|
+
mkdirSync(join(staging, "usr/lib"), { recursive: true });
|
|
160
|
+
mkdirSync(join(staging, "etc"), { recursive: true });
|
|
161
|
+
copyFile(join(build, "os-release"), join(staging, "usr/lib/os-release"));
|
|
162
|
+
link("../usr/lib/os-release", join(staging, "etc/os-release"));
|
|
163
|
+
// LOGO names an icon-theme icon; the standard lookup paths point at the
|
|
164
|
+
// icon /buninu already ships.
|
|
165
|
+
mkdirSync(join(staging, "usr/share/icons/hicolor/512x512/apps"), { recursive: true });
|
|
166
|
+
mkdirSync(join(staging, "usr/share/pixmaps"), { recursive: true });
|
|
167
|
+
link("../../../../../../buninu/icon.png", join(staging, "usr/share/icons/hicolor/512x512/apps/buninu-linux.png"));
|
|
168
|
+
link("../../../buninu/icon.png", join(staging, "usr/share/pixmaps/buninu-linux.png"));
|
|
169
|
+
|
|
170
|
+
normalizeModes(staging);
|
|
171
|
+
const cpio = spawnSync(jsRuntime(), [
|
|
172
|
+
join(rootDir, "scripts/cpio.js"),
|
|
173
|
+
staging,
|
|
174
|
+
join(build, "initramfs.cpio.gz"),
|
|
175
|
+
...devices(cfg),
|
|
176
|
+
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
177
|
+
if (cpio.status !== 0) throw new Error("scripts/cpio.js failed");
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(staging, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
console.log(`Built build/initramfs.cpio.gz for ${cfg.arch}`);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
runIfMain(import.meta.url, () => pack());
|
package/scripts/uki.js
CHANGED
|
File without changes
|