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 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
- "fakeroot", "cpio", "gzip",
55
- `${architectures[arch].binutils}-objcopy`, `${architectures[arch].binutils}-objdump`,
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"
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.18 - 2026-09-29
4
+
5
+ - Sync jsgotty to 1.1.14: Kitty for Windows
6
+ - Sync bunmsh to 0.3.10: Fix several posix/gnu/external builtin commands
7
+
3
8
  ## 0.4.17 - 2026-09-26
4
9
 
5
10
  - readme: Add homepage URL
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable user-visible changes to bunmsh are documented here.
4
4
 
5
+ ## [0.3.10] - 2026-09-29
6
+
7
+ ### Fixed
8
+
9
+ - Match system utilities when handling multiple operands: `printf` repeats its
10
+ format for remaining arguments; `ln` links multiple sources into a directory;
11
+ `head`, `tail`, `wc`, and `cut` process each file separately; and `md5sum` and
12
+ `sha256sum` print one labelled digest per file. `wc` also prints a total for
13
+ multiple files. Reference tests compare these results with system commands.
14
+
15
+ - Make `grep -r PATTERN` without an explicit path search the current directory,
16
+ as system `grep` does, instead of reading a locked standard-input stream.
17
+ Reference tests cover recursive matches, line numbers, no matches, and quiet
18
+ mode.
19
+
5
20
  ## [0.3.9] - 2026-09-22
6
21
 
7
22
  ### Fixed
@@ -32,8 +32,12 @@ pseudo-terminal tests.
32
32
 
33
33
  ### Grammar and execution
34
34
 
35
- - Background jobs (`&`), job control, coprocesses, and asynchronous command
36
- lists are not implemented.
35
+ - A final, standalone, unquoted `&` starts a single external command in the
36
+ background (trailing comments and newlines are allowed). Stdin is ignored;
37
+ stdout/stderr are inherited, and the process is exposed in `Bun.sha.procs`.
38
+ Background pipelines, builtins, functions, and all redirections are rejected
39
+ before files are opened. General asynchronous command lists, job control,
40
+ and coprocesses are not implemented.
37
41
  - Brace command groups, `select`, arithmetic commands, and `[[ ... ]]` are
38
42
  not implemented.
39
43
  - Functions and subshells cover practical cases but do not yet reproduce every
@@ -959,7 +959,8 @@ Create hard or symbolic links.
959
959
  ### Usage
960
960
 
961
961
  ```sh
962
- ln [-sfT] TARGET LINK
962
+ ln [-sf] TARGET LINK
963
+ ln [-sf] TARGET ... DIRECTORY
963
964
  ```
964
965
 
965
966
  ### Options and forms
@@ -967,6 +968,7 @@ ln [-sfT] TARGET LINK
967
968
  - `-s`: Create a symbolic link.
968
969
  - `-f`: Replace an existing destination.
969
970
  - `-T`: Treat the destination as a normal path, not a directory.
971
+ - Multiple targets may be linked into an existing directory.
970
972
 
971
973
  ### Example
972
974
 
@@ -5,7 +5,8 @@ Create hard or symbolic links.
5
5
  ### Usage
6
6
 
7
7
  ```sh
8
- ln [-sfT] TARGET LINK
8
+ ln [-sf] TARGET LINK
9
+ ln [-sf] TARGET ... DIRECTORY
9
10
  ```
10
11
 
11
12
  ### Options and forms
@@ -13,6 +14,7 @@ ln [-sfT] TARGET LINK
13
14
  - `-s`: Create a symbolic link.
14
15
  - `-f`: Replace an existing destination.
15
16
  - `-T`: Treat the destination as a normal path, not a directory.
17
+ - Multiple targets may be linked into an existing directory.
16
18
 
17
19
  ### Example
18
20
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmsh",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Cross-platform command shell for Bun JavaScript",
5
5
  "license": "MIT",
6
6
  "type": "module",