buninu-linux 0.7.2 → 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/initramfs/buninu/CHANGELOG.md +5 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +15 -0
- package/initramfs/buninu/apps/bunmsh/PORTING.md +6 -2
- package/initramfs/buninu/apps/bunmsh/help/README.md +3 -1
- package/initramfs/buninu/apps/bunmsh/help/ln.md +3 -1
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/shell.js +173 -84
- package/initramfs/buninu/apps/bunmsh/test/reference.test.js +95 -1
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +91 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-microsoft-terminal-conpty.txt +21 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-ws.txt +20 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/NOTICE-microsoft-terminal-conpty.md +535 -0
- package/initramfs/buninu/apps/jsgotty/NOTICE +100 -0
- package/initramfs/buninu/apps/jsgotty/conpty/OpenConsole.exe +0 -0
- package/initramfs/buninu/apps/jsgotty/conpty/conpty.dll +0 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +24 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +6 -2
- package/initramfs/buninu/apps/jsgotty/windows-conpty.js +135 -0
- package/initramfs/buninu/package.json +1 -1
- package/package.json +1 -1
- package/scripts/cpio.js +107 -0
- package/scripts/pack-initramfs.sh +16 -0
- package/scripts/uki.js +136 -0
|
@@ -115,6 +115,26 @@ function assetFilePath(key) {
|
|
|
115
115
|
return assets ? assets.assetDiskPath(key) : path.join(__dirname, key);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function windowsConptyDllPath() {
|
|
119
|
+
const dll = assetFilePath("conpty/conpty.dll");
|
|
120
|
+
if (fs.existsSync(dll)) return dll;
|
|
121
|
+
if (!assets) return null;
|
|
122
|
+
try {
|
|
123
|
+
const dllBytes = assets.readAssetBytesSync("conpty/conpty.dll");
|
|
124
|
+
const hostBytes = assets.readAssetBytesSync("conpty/OpenConsole.exe");
|
|
125
|
+
const version = crypto.createHash("sha256").update(dllBytes).digest("hex").slice(0, 16);
|
|
126
|
+
const dir = path.join(os.tmpdir(), `jsgotty-conpty-${version}`);
|
|
127
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
128
|
+
for (const [name, bytes] of [["conpty.dll", dllBytes], ["OpenConsole.exe", hostBytes]]) {
|
|
129
|
+
const dest = path.join(dir, name);
|
|
130
|
+
if (!fs.existsSync(dest)) fs.writeFileSync(dest, bytes);
|
|
131
|
+
}
|
|
132
|
+
return path.join(dir, "conpty.dll");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new Error(`Could not prepare bundled ConPTY: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
// URL-relative path -> asset key, or null when it escapes `static/`.
|
|
119
139
|
// Normalizing against "/" is what makes "../../etc/passwd" unreachable.
|
|
120
140
|
function staticAssetKey(relativePath) {
|
|
@@ -1624,6 +1644,10 @@ function createPtyBackend(options) {
|
|
|
1624
1644
|
if (globalThis.Bun?.semver?.satisfies?.(
|
|
1625
1645
|
globalThis.Bun?.version, ">=1.3.14"
|
|
1626
1646
|
)) {
|
|
1647
|
+
const conptyDll = windowsConptyDllPath();
|
|
1648
|
+
if (conptyDll) {
|
|
1649
|
+
return new (require("./windows-conpty.js"))(options, conptyDll);
|
|
1650
|
+
}
|
|
1627
1651
|
return new BunPtyBackend(options);
|
|
1628
1652
|
}
|
|
1629
1653
|
return new NodePtyBackend(options);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.14",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Share your terminal as a web application: A JavaScript rewrite of GoTTY",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "commonjs",
|
|
10
10
|
"assets": [
|
|
11
|
+
"conpty",
|
|
11
12
|
"static",
|
|
12
13
|
"README.md",
|
|
13
|
-
"README.en.md"
|
|
14
|
+
"README.en.md",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"NOTICE",
|
|
17
|
+
"LICENSES"
|
|
14
18
|
],
|
|
15
19
|
"dependencies": {
|
|
16
20
|
"node-addon-api": "^7.1.1",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Windows PTY adapter for the out-of-band Microsoft Console package.
|
|
2
|
+
// Keep the same interface as BunPtyBackend in gotty.js.
|
|
3
|
+
const { dlopen, ptr } = require("bun:ffi");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const kernel = dlopen("kernel32.dll", {
|
|
7
|
+
CreatePipe: { args: ["ptr", "ptr", "ptr", "u32"], returns: "i32" },
|
|
8
|
+
InitializeProcThreadAttributeList: { args: ["ptr", "u32", "u32", "ptr"], returns: "i32" },
|
|
9
|
+
UpdateProcThreadAttribute: { args: ["ptr", "u32", "usize", "ptr", "usize", "ptr", "ptr"], returns: "i32" },
|
|
10
|
+
DeleteProcThreadAttributeList: { args: ["ptr"], returns: "void" },
|
|
11
|
+
CreateProcessW: { args: ["ptr", "ptr", "ptr", "ptr", "i32", "u32", "ptr", "ptr", "ptr", "ptr"], returns: "i32" },
|
|
12
|
+
PeekNamedPipe: { args: ["ptr", "ptr", "u32", "ptr", "ptr", "ptr"], returns: "i32" },
|
|
13
|
+
ReadFile: { args: ["ptr", "ptr", "u32", "ptr", "ptr"], returns: "i32" },
|
|
14
|
+
WriteFile: { args: ["ptr", "ptr", "u32", "ptr", "ptr"], returns: "i32" },
|
|
15
|
+
GetExitCodeProcess: { args: ["ptr", "ptr"], returns: "i32" },
|
|
16
|
+
TerminateProcess: { args: ["ptr", "u32"], returns: "i32" },
|
|
17
|
+
CloseHandle: { args: ["ptr"], returns: "i32" },
|
|
18
|
+
GetLastError: { args: [], returns: "u32" },
|
|
19
|
+
}).symbols;
|
|
20
|
+
|
|
21
|
+
const conptySymbols = {
|
|
22
|
+
ConptyCreatePseudoConsole: { args: ["u32", "ptr", "ptr", "u32", "ptr"], returns: "i32" },
|
|
23
|
+
ConptyResizePseudoConsole: { args: ["ptr", "u32"], returns: "i32" },
|
|
24
|
+
ConptyClosePseudoConsole: { args: ["ptr"], returns: "void" },
|
|
25
|
+
};
|
|
26
|
+
const wide = (value) => Buffer.from(`${value}\0`, "utf16le");
|
|
27
|
+
const handle = (buffer) => Number(buffer.readBigUInt64LE(0));
|
|
28
|
+
const size = (cols, rows) => ((Math.max(1, rows) & 0xffff) << 16) | (Math.max(1, cols) & 0xffff);
|
|
29
|
+
const winError = (operation) => new Error(`${operation} failed: ${kernel.GetLastError()}`);
|
|
30
|
+
// Windows CRT argv escaping: double backslashes before quotes and at the end.
|
|
31
|
+
function quoteArg(value) {
|
|
32
|
+
let result = '"', slashes = 0;
|
|
33
|
+
for (const char of String(value)) {
|
|
34
|
+
if (char === "\\") { slashes++; continue; }
|
|
35
|
+
if (char === '"') result += "\\".repeat(slashes * 2 + 1) + '"';
|
|
36
|
+
else result += "\\".repeat(slashes) + char;
|
|
37
|
+
slashes = 0;
|
|
38
|
+
}
|
|
39
|
+
return result + "\\".repeat(slashes * 2) + '"';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class WindowsConptyBackend {
|
|
43
|
+
constructor(options, dllPath) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
this.dataHandlers = [];
|
|
46
|
+
this.exitHandlers = [];
|
|
47
|
+
this.closed = false;
|
|
48
|
+
this.conpty = dlopen(path.resolve(dllPath), conptySymbols).symbols;
|
|
49
|
+
const inputRead = Buffer.alloc(8), inputWrite = Buffer.alloc(8);
|
|
50
|
+
const outputRead = Buffer.alloc(8), outputWrite = Buffer.alloc(8);
|
|
51
|
+
if (!kernel.CreatePipe(inputRead, inputWrite, 0, 0)) throw winError("CreatePipe input");
|
|
52
|
+
if (!kernel.CreatePipe(outputRead, outputWrite, 0, 0)) throw winError("CreatePipe output");
|
|
53
|
+
this.input = handle(inputWrite);
|
|
54
|
+
this.output = handle(outputRead);
|
|
55
|
+
const pseudo = Buffer.alloc(8);
|
|
56
|
+
const hr = this.conpty.ConptyCreatePseudoConsole(
|
|
57
|
+
size(options.width || 80, options.height || 24), handle(inputRead), handle(outputWrite), 0, pseudo);
|
|
58
|
+
kernel.CloseHandle(handle(inputRead));
|
|
59
|
+
kernel.CloseHandle(handle(outputWrite));
|
|
60
|
+
if (hr !== 0) throw new Error(`ConptyCreatePseudoConsole failed: ${hr}`);
|
|
61
|
+
this.pseudo = handle(pseudo);
|
|
62
|
+
|
|
63
|
+
const listSize = Buffer.alloc(8);
|
|
64
|
+
kernel.InitializeProcThreadAttributeList(0, 1, 0, listSize);
|
|
65
|
+
const list = Buffer.alloc(Number(listSize.readBigUInt64LE(0)));
|
|
66
|
+
if (!kernel.InitializeProcThreadAttributeList(list, 1, 0, listSize)) throw winError("InitializeProcThreadAttributeList");
|
|
67
|
+
if (!kernel.UpdateProcThreadAttribute(list, 0, 0x00020016, this.pseudo, 8, 0, 0)) throw winError("UpdateProcThreadAttribute");
|
|
68
|
+
const startup = Buffer.alloc(112);
|
|
69
|
+
startup.writeUInt32LE(112, 0);
|
|
70
|
+
startup.writeUInt32LE(0x100, 60); // STARTF_USESTDHANDLES
|
|
71
|
+
startup.writeBigUInt64LE(BigInt(ptr(list)), 104);
|
|
72
|
+
const processInfo = Buffer.alloc(24);
|
|
73
|
+
const env = { ...process.env, TERM: "xterm-256color", ...(options.headerEnv || {}) };
|
|
74
|
+
const envBlock = wide(Object.entries(env).filter(([key]) => !key.startsWith("="))
|
|
75
|
+
.map(([key, value]) => `${key}=${value}`).join("\0") + "\0");
|
|
76
|
+
const executable = Bun.which(options.command) || options.command;
|
|
77
|
+
const command = wide([executable, ...options.argv].map(quoteArg).join(" "));
|
|
78
|
+
const ok = kernel.CreateProcessW(wide(executable), command, 0, 0, 0,
|
|
79
|
+
0x00080000 | 0x00000400, envBlock, wide(process.cwd()), startup, processInfo);
|
|
80
|
+
kernel.DeleteProcThreadAttributeList(list);
|
|
81
|
+
if (!ok) throw winError("CreateProcessW");
|
|
82
|
+
this.process = handle(processInfo);
|
|
83
|
+
this.pid = processInfo.readUInt32LE(16);
|
|
84
|
+
kernel.CloseHandle(Number(processInfo.readBigUInt64LE(8)));
|
|
85
|
+
this.pollTimer = setInterval(() => this.poll(), 5);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onData(handler) { this.dataHandlers.push(handler); }
|
|
89
|
+
onExit(handler) { this.exitHandlers.push(handler); }
|
|
90
|
+
write(buffer) { this.writeBinary(buffer); }
|
|
91
|
+
writeBinary(buffer) {
|
|
92
|
+
const data = Buffer.from(buffer);
|
|
93
|
+
const written = Buffer.alloc(4);
|
|
94
|
+
for (let offset = 0; offset < data.length;) {
|
|
95
|
+
const chunk = data.subarray(offset);
|
|
96
|
+
if (!kernel.WriteFile(this.input, chunk, chunk.length, written, 0)) throw winError("WriteFile");
|
|
97
|
+
const count = written.readUInt32LE(0);
|
|
98
|
+
if (!count) throw new Error("WriteFile wrote no data");
|
|
99
|
+
offset += count;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
resize(cols, rows) {
|
|
103
|
+
const hr = this.conpty.ConptyResizePseudoConsole(this.pseudo, size(cols, rows));
|
|
104
|
+
if (hr !== 0) throw new Error(`ConptyResizePseudoConsole failed: ${hr}`);
|
|
105
|
+
}
|
|
106
|
+
kill() { if (!this.closed) kernel.TerminateProcess(this.process, 1); }
|
|
107
|
+
close() { this.kill(); }
|
|
108
|
+
|
|
109
|
+
poll() {
|
|
110
|
+
if (this.closed) return;
|
|
111
|
+
const available = Buffer.alloc(4), readCount = Buffer.alloc(4);
|
|
112
|
+
if (kernel.PeekNamedPipe(this.output, 0, 0, 0, available, 0)) {
|
|
113
|
+
for (let bytes = available.readUInt32LE(0); bytes > 0; bytes = available.readUInt32LE(0)) {
|
|
114
|
+
const data = Buffer.alloc(Math.min(bytes, 65536));
|
|
115
|
+
if (!kernel.ReadFile(this.output, data, data.length, readCount, 0)) break;
|
|
116
|
+
const count = readCount.readUInt32LE(0);
|
|
117
|
+
if (!count) break;
|
|
118
|
+
for (const handler of this.dataHandlers) handler(data.subarray(0, count));
|
|
119
|
+
if (!kernel.PeekNamedPipe(this.output, 0, 0, 0, available, 0)) break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const exitCode = Buffer.alloc(4);
|
|
123
|
+
if (kernel.GetExitCodeProcess(this.process, exitCode) && exitCode.readUInt32LE(0) !== 259) {
|
|
124
|
+
this.closed = true;
|
|
125
|
+
clearInterval(this.pollTimer);
|
|
126
|
+
this.conpty.ConptyClosePseudoConsole(this.pseudo);
|
|
127
|
+
kernel.CloseHandle(this.input);
|
|
128
|
+
kernel.CloseHandle(this.output);
|
|
129
|
+
kernel.CloseHandle(this.process);
|
|
130
|
+
for (const handler of this.exitHandlers) handler();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = WindowsConptyBackend;
|
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(", ")}`);
|