buninu-linux 0.7.3 → 0.7.5

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 CHANGED
@@ -103,83 +103,83 @@ the built image redistributes them.
103
103
  - License: GPL-2.0-only WITH Linux-syscall-note
104
104
  - See: `LICENSES/GPL-2.0.txt` and `LICENSES/Linux-syscall-note.txt`
105
105
 
106
- Linux 6.18.53 as packaged by Alpine Linux 3.24 from the `linux-lts` aport.
107
- The default build uses package `linux-virt`, version 6.18.53-r0.
108
- `--linux-lts` and `--real` use package `linux-lts`, version 6.18.53-r0.
106
+ Linux 6.18.54 as packaged by Alpine Linux 3.24 from the `linux-lts` aport.
107
+ The default build uses package `linux-virt`, version 6.18.54-r0.
108
+ `--linux-lts` and `--real` use package `linux-lts`, version 6.18.54-r0.
109
109
  Each comes from the x86_64 or aarch64 repository to match `--arch`. All
110
110
  listed modules are the selected package's `.ko.gz` files decompressed without
111
111
  modification.
112
112
 
113
- - Upstream source: https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.53.tar.xz
113
+ - Upstream source: https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.54.tar.xz
114
114
  - Alpine build recipe, configuration and patches (Corresponding Source),
115
- pinned to the commit shipping 6.18.53-r0 (the same for both architectures):
116
- https://gitlab.alpinelinux.org/alpine/aports/-/blob/00abeb21803f818099833e761976a578dd1c0380/main/linux-lts/APKBUILD
115
+ pinned to the commit shipping 6.18.54-r0 (the same for both architectures):
116
+ https://gitlab.alpinelinux.org/alpine/aports/-/blob/f8254a6d11c37b153dae6a0fd98ba378dd0d093d/main/linux-lts/APKBUILD
117
117
  - Packages:
118
- https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/linux-virt-6.18.53-r0.apk
119
- SHA-256 `cad859cc46342e18002621fdde166bf2cd520dfec5e781d241de7d13e53970d6`
120
- https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/linux-lts-6.18.53-r0.apk
121
- SHA-256 `8e3cfdd1d98e0e70c2e70a8c299ed3cd1e0939d60ad7bbd1da1cf241d794490b`
122
- https://dl-cdn.alpinelinux.org/alpine/v3.24/main/aarch64/linux-virt-6.18.53-r0.apk
123
- SHA-256 `fea61b7fd5e72e626d771f9798df56c83db658d0e221044f06fc28aacf5a118b`
124
- https://dl-cdn.alpinelinux.org/alpine/v3.24/main/aarch64/linux-lts-6.18.53-r0.apk
125
- SHA-256 `46885041022ea11afa1e07c40199c74146bf72d362368a844338af0c162e365c`
118
+ https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/linux-virt-6.18.54-r0.apk
119
+ SHA-256 `57a522d2b6e9d1b6c9de6b9f7fc42aa4b8d33ebf9f2b42b4251dbee6ff2d9839`
120
+ https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/linux-lts-6.18.54-r0.apk
121
+ SHA-256 `1d02a1b74a8a09c476a3c01782c0b8613ae2935988e65d4d31e6bef44dfb8b64`
122
+ https://dl-cdn.alpinelinux.org/alpine/v3.24/main/aarch64/linux-virt-6.18.54-r0.apk
123
+ SHA-256 `9a4a6fa042b60b70f453dded99762810516b35793c64d776d75986b0b110b6e0`
124
+ https://dl-cdn.alpinelinux.org/alpine/v3.24/main/aarch64/linux-lts-6.18.54-r0.apk
125
+ SHA-256 `f945ddcc306546c7c22cf10249f637fc6e806ecc40286fb50950558dd573e7e9`
126
126
 
127
127
  SHA-256 of files in the default x86_64 `linux-virt` image:
128
128
 
129
129
  ```text
130
- kernel/x86_64/vmlinuz-virt 3b6e001d41938fdf4fab7d87826fc40dfab733973a5ef96a14c892dbdec87502
131
- failover.ko f52f8dedf68be2a228e84d796170471c7103d88910a14e2307484fe94c4d1a11
132
- net_failover.ko 8fe715d813a1e795cf0b46d54fd67f6c625dfabf1a4eebfeec96ae34f8aab0b1
133
- virtio_net.ko cdf49d6216ba92313d3e40f065c326d86eae654bcbb811ac53e7c2613e686f1e
130
+ kernel/x86_64/vmlinuz-virt c7ce829b618d4a9d2df79c58ea0fb2a392e606f6c47bb94696ea75610a568166
131
+ failover.ko c02fc21655bd3a8598ddf29f1028a1029bcb19b1da8611d3cf3c8685d0412f84
132
+ net_failover.ko 46e0c3d3f4cdfafabffbd1a57ef59aac0bcc0f1a4e22b114d992cd46ac264145
133
+ virtio_net.ko 1163e07fd5391849820ede09277b0147ae7e48ef0f08f9e96da08bb2ff131c2a
134
134
  ```
135
135
 
136
136
  SHA-256 of files in an x86_64 `--linux-lts` image:
137
137
 
138
138
  ```text
139
- kernel/x86_64/vmlinuz-lts 1b2ba2cad7973637f0f5589f845f7b773e73920e69d26785d2ff21c6c8995dc7
140
- failover.ko 0fcd683e5023a3910bac2d2eb02eff349ce00d81b572f4a9093a0c70fc1c77f3
141
- net_failover.ko 613809bd8d4c283b7a2b9dba5d8466cc36f478772a86e8c89de5e792bce69207
142
- virtio_net.ko 1fb4ce7a0b546933b1e40d67b3570aec808307dcafead342d3f4bcd5a0523b7c
143
- virtio_ring.ko eba2c109827a5d3b0e869505268a68d72cd6f2f9cc68778b66145c0503d9561f
144
- virtio.ko 00d107d78dc1d47a4eea335c6d6b28026ae2c783a9b7629f2cc080bdbc8d2dc3
145
- virtio_pci_legacy_dev.ko 370b8ad3521cbd3934c78eacc5965b20eff29c55c7fcded023c75c5d21424841
146
- virtio_pci_modern_dev.ko 572d719ccad98342ebce909b8dac65ef64379c3a6c97e2d1fb38ff66950d9322
147
- virtio_pci.ko e64039e30729e535b3ce9394249c79147bab27dfd20b7cf9914cd635dda95fc3
139
+ kernel/x86_64/vmlinuz-lts d3cb46d949f3d05a7e6753ba0b9a054323e6a7b6eeb231b31a1659d8f9f51a28
140
+ failover.ko 3c0e33c374419512c410559561e8bc5d2fac573cb449da404bbeaf94ec7641fe
141
+ net_failover.ko 2a5db89834e5f35ecdf114da8dbe4d4db4061371d37768d9f92eb09d479d21ab
142
+ virtio_net.ko 1fc0c4992e4a9ad83985fbad5a5674bacceb4c8ab5329b4509656ba3350478eb
143
+ virtio_ring.ko 3c14098101a9f30f034410efe7e91e369057d1709152a0419a3a9fc4b1367915
144
+ virtio.ko f47dac1696dcab539c5d313f13bd5e06945cf921bf406e0f709e1866de72803d
145
+ virtio_pci_legacy_dev.ko 662c0f09bcb88c7c8a79fb2159d4aa0b62044d1859a46de03fc9e57fc3501931
146
+ virtio_pci_modern_dev.ko 006e525fda5e369b6a1669ec0e8e174c784f1ecdb7dcfb6257ffc67fd5b93525
147
+ virtio_pci.ko 94d66ebd7f2a325ce83c9f42175432a7797969b3e3c7fcc07d2b913012f1c77f
148
148
  ```
149
149
 
150
150
  `--real` additionally includes the following modules from that same
151
151
  x86_64 `linux-lts` package for physical xHCI controllers and USB HID keyboards:
152
152
 
153
153
  ```text
154
- usb-common.ko 3c7f2f1de34cafbf64fa3528862539cbe8cd3830cd97110e8401b510835c8761
155
- usbcore.ko 0fce52fd1836304d48d25ef322558f27304ea9247c5b234f8ac2ecb9d8f00597
156
- xhci-hcd.ko 22e687fe561127ea4cd1aba21058cbf253d0541ccf315555004f5a94ed7ab727
157
- xhci-pci.ko 82eb0ea3869da82d1762130a738f2ff3ae6d77b16670f69f7f09cff4e8f3e360
158
- xhci-pci-renesas.ko 07df52a6ee408723938bc8a32cb4a2bfe4cbcf014ff48d23fd7f7ab41c201f6f
159
- hid.ko fbe4561a8dd65cd26160cbe9b7c5af23c7920044771e2e08a8aee6aa71141053
160
- hid-generic.ko acfda4f50e19c055e5d719ebc2b9b8f8c6c076552592dfaabccbe128a9862694
161
- usbhid.ko 12d3648467db87c2904a11a076c17768d8d51c28fe8ec41046e7d961592a81f1
154
+ usb-common.ko 471bed173d18c373c80e9cce646f75b71b034b3b398e6bd04c51fa065ac92e6b
155
+ usbcore.ko 99b20afa2057720f2aa36224909c37740cbe1068e23ba52872ed09e6c7966c29
156
+ xhci-hcd.ko b3ffac428a78858532a5114ad4f2f66de253aba32932f508d62eceb29af39313
157
+ xhci-pci.ko 2a924cfba63360acf3a9a688a5fb69f87c2c82a02f904e972a9f5c9cb303588f
158
+ xhci-pci-renesas.ko 2c8fa8dac908da5a13c8f87e520cc1d4a026aeb9ee58d1ac0380d696c8ef6a72
159
+ hid.ko 3aa16634ee8621291abfa1f9efca287299dea052678c8ead5af86003a7e493f3
160
+ hid-generic.ko 2879395175b39a47f09e81bb00cb1a4b565103ba4583ce99169b9ef7fd943d62
161
+ usbhid.ko e80b60c6dd99a87c95bd97434679aa3f069c9bce2f67a7311f179b3066cdea59
162
162
  ```
163
163
 
164
164
  SHA-256 of files in the default aarch64 `linux-virt` image:
165
165
 
166
166
  ```text
167
- kernel/aarch64/vmlinuz-virt 9884ee00ecfff6a0dc9821cc9981b3aca74f741950704776b4335b1e076d96e6
168
- failover.ko 61b052e5ed3ef01bc6f71d84acb79bfe8cf2e5168f625fce76630dec14bf8b99
169
- net_failover.ko a0b150f46c7e9aa48156a8b94916ab65a4d8947143b381af56da677436584259
170
- virtio_net.ko e9c007778d2b0a7d4986cd04b724965a01ffee05ada92f75ce13bf59bef2491c
167
+ kernel/aarch64/vmlinuz-virt 29de2da2ea7aa1d95da37c947278caca65ef3ad211dbb92ac75a6b962f96d0d2
168
+ failover.ko 62bfac505026e368b291e8d7e167dc90bff826c44d0db1ff4340de2201022674
169
+ net_failover.ko 3761f107b61ae4d11a7bed402cdab1862f3d61cacc612b2e6d9bc61ad1e20d6d
170
+ virtio_net.ko 22a95296a008f009de59e933cc1b9615396612ef5eb3922fac2b79ac672a1305
171
171
  ```
172
172
 
173
173
  SHA-256 of files in an aarch64 `--linux-lts` image:
174
174
 
175
175
  ```text
176
- kernel/aarch64/vmlinuz-lts 00426cce3a4b2b4972043eeb68a3a2af5645eb7a50ca853c375e41944ac66d27
177
- failover.ko b9bea5bb0c956b58faf0f803a205dafd889bfda75e7c59d234f60e65145fbec8
178
- net_failover.ko d8eb3087b75aba891e93327c6c43f37362cfe49421b7cabfd12f57a34d5c215a
179
- virtio_net.ko 10e73397bbb424b4bdb86781297f6fc30b13091e9a77fc7059e4861c09ef609d
180
- virtio_pci_legacy_dev.ko 476c6762743b9a5789200a31ee339173610f975b42e588914aabf41d8ade2958
181
- virtio_pci_modern_dev.ko 595618d12b67c67a0f29c406f6578d10d010024ac83014855672fd3fd695d6ed
182
- virtio_pci.ko 545a599cb7136a509a33cd3b52e80a8cd2bfa10668b0e766dc52872ce07d510b
176
+ kernel/aarch64/vmlinuz-lts 52521d144fe344f23ffd03297fd0e0da42578e54d3da18685cbb7f613244c001
177
+ failover.ko 73571d1192683fa4f6346d7082920b4ac1a3b2edfb2da9e08ec13269dcb71f5e
178
+ net_failover.ko ba1700cf056585843488a5a8e75884d16cee45b4d40d8ace2f977e697cb5557c
179
+ virtio_net.ko fea67b801481f1445be419541e7a352a07019d52acbf644fe01f21c51d8d1475
180
+ virtio_pci_legacy_dev.ko 0cbfc93f3ef238c2565187eb8be94d1d0127bf2fe521f28b75245db12f4331ff
181
+ virtio_pci_modern_dev.ko 3fba75f38825df5ab616492bf3fb3329c4aed1a8b7f9a98370706e70a14fc2a3
182
+ virtio_pci.ko 92652ff84904b2b2dda1549d362ec97377f954da5463575f7e35a4ef5847c3ad
183
183
  ```
184
184
 
185
185
  ### linuxx64.efi.stub and linuxaa64.efi.stub
package/README.md CHANGED
@@ -64,16 +64,23 @@
64
64
  - This guide builds a bootable `buninu-linux-<version>.img`
65
65
  - The documented build environment is Debian 13 under Termux PRoot
66
66
  - A regular Debian installation works as well.
67
+ * Experimental Alpine support now!
67
68
  - For a source checkout, clone into the Termux home so native Termux and PRoot
68
69
  can share it; `~` differs between them, but the absolute path is the same.
69
70
 
70
71
  ### Steps
71
72
 
72
- - Before anything, install Bun in Debian first:
73
+ - Before anything, install Bun in Debian/Alpine first:
73
74
 
74
75
  ```sh
76
+ # Debian/Ubuntu
75
77
  apt update
76
78
  apt install curl unzip
79
+
80
+ # Alpine
81
+ apk update
82
+ apk add curl unzip bash libgcc libstdc++
83
+
77
84
  curl -fsSL https://bun.sh/install | bash
78
85
  ```
79
86
 
@@ -85,8 +92,12 @@ Install the build tools, enter a directory where
85
92
  you want to keep the finished image, then run the published package:
86
93
 
87
94
  ```sh
95
+ # Debian/Ubuntu
88
96
  apt install binutils-mingw-w64-x86-64 cpio curl dosfstools fakeroot mtools parted unzip
89
97
 
98
+ # Alpine
99
+ apk add bash coreutils tar curl unzip parted dosfstools mtools libgcc libstdc++
100
+
90
101
  mkdir -p /data/data/com.termux/files/home/buninu-build
91
102
  cd /data/data/com.termux/files/home/buninu-build
92
103
 
@@ -94,7 +105,14 @@ export PATH=$HOME/.bun/bin:$PATH
94
105
 
95
106
  bun x buninu-linux --version
96
107
 
108
+ # Debian/Ubuntu
97
109
  bun x buninu-linux -fb --real --export
110
+
111
+ # Alpine
112
+ export BUNINU_JS_CPIO=1
113
+ export BUNINU_JS_UKI=1
114
+ bun x buninu-linux -fb --real --export
115
+
98
116
  ```
99
117
 
100
118
  `--export` copies the completed `vda.img` out of bunx's package directory and
@@ -890,7 +908,7 @@ initramfs/ init.js, commands, userspace: the same for every archi
890
908
  native/<arch>/ musl and the GCC runtime (committed); bun and modules (fetched)
891
909
  ```
892
910
 
893
- The `--real` inputs are Alpine v3.24 `linux-lts-6.18.53-r0`, musl `1.2.6-r2`,
911
+ The `--real` inputs are Alpine v3.24 `linux-lts-6.18.54-r0`, musl `1.2.6-r2`,
894
912
  `systemd-efistub-260.2-r0`, `libstdc++`/`libgcc` `15.2.0-r5`, and Bun 1.4.2
895
913
  `linux-x64-musl-baseline`. The aarch64 guest uses the same versions from
896
914
  Alpine's aarch64 repository and Bun's `linux-aarch64-musl`. There is no BusyBox
@@ -987,7 +1005,7 @@ The physical boot chain is firmware → the UKI stub → its embedded
987
1005
  and, with the currently pinned kernel, embeds this complete command line:
988
1006
 
989
1007
  ```text
990
- console=ttyS0,115200 console=tty0 REAL_MACHINE=1 panic=0 PATH=/bin KERNEL_RELEASE=6.18.53-0-lts rdinit=/bin/bun -- -e import('/init.js')
1008
+ console=ttyS0,115200 console=tty0 REAL_MACHINE=1 panic=0 PATH=/bin KERNEL_RELEASE=6.18.54-0-lts rdinit=/bin/bun -- -e import('/init.js')
991
1009
  ```
992
1010
 
993
1011
  Serial kernel logging is retained, while the final console makes
@@ -1011,7 +1029,7 @@ the same way). Build without `--real`; this selects Alpine `linux-virt`, omits t
1011
1029
  module set, and puts `ttyS0` last so serial stdio owns `/dev/console`:
1012
1030
 
1013
1031
  ```text
1014
- console=tty0 console=ttyS0,115200 panic=0 PATH=/bin KERNEL_RELEASE=6.18.53-0-virt rdinit=/bin/bun -- -e import('/init.js')
1032
+ console=tty0 console=ttyS0,115200 panic=0 PATH=/bin KERNEL_RELEASE=6.18.54-0-virt rdinit=/bin/bun -- -e import('/init.js')
1015
1033
  ```
1016
1034
 
1017
1035
  Build and boot it with:
@@ -1128,7 +1146,7 @@ The default `--real` image embeds this kernel command line (shown for the
1128
1146
  currently pinned LTS release):
1129
1147
 
1130
1148
  ```text
1131
- console=ttyS0,115200 console=tty0 REAL_MACHINE=1 panic=0 PATH=/bin KERNEL_RELEASE=6.18.53-0-lts rdinit=/bin/bun -- -e import('/init.js')
1149
+ console=ttyS0,115200 console=tty0 REAL_MACHINE=1 panic=0 PATH=/bin KERNEL_RELEASE=6.18.54-0-lts rdinit=/bin/bun -- -e import('/init.js')
1132
1150
  ```
1133
1151
 
1134
1152
  The physical display (`tty0`) is the final console and therefore owns PID 1's
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/fetch-alpine.sh CHANGED
@@ -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
 
package/fetch-bun.sh CHANGED
@@ -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
 
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
@@ -109,6 +120,10 @@ initramfs/init.js needs, -r just boots what is already there. Anything after
109
120
  -- goes to qemu-system-x86_64 or qemu-system-aarch64 verbatim (for example:
110
121
  -r -- -smp 4).
111
122
 
123
+ With BUNINU_JS_TOOLS=1 in the environment, fetch and build are done in
124
+ JavaScript (scripts/fetch.js, scripts/build.js) and need only node or bun:
125
+ no Debian toolchain and no Docker.
126
+
112
127
  Fetch and build need the Debian toolchain from README "Build environment"
113
128
  (PRoot, a Debian or Ubuntu host, or --docker); run needs QEMU (Termux
114
129
  pkg, apt, or brew install qemu). Tools missing for the stages you picked are
@@ -149,6 +164,8 @@ function parse(argv) {
149
164
  } else if (argument === "--readme") {
150
165
  printReadme();
151
166
  process.exit(0);
167
+ } else if (argument === "--check") {
168
+ runCheck(argv.slice(index + 1));
152
169
  } else if (argument === "--linux-lts") {
153
170
  linuxLts = true;
154
171
  } else if (argument === "--real") {
@@ -217,6 +234,14 @@ function printReadme() {
217
234
  process.stdout.write(rendered.endsWith("\n") ? rendered : rendered + "\n");
218
235
  }
219
236
 
237
+ // --check hands everything after it to check_build.js, under the same
238
+ // runtime, and exits with its status.
239
+ function runCheck(args) {
240
+ const result = spawnSync(process.execPath, [resolve(rootDir, "check_build.js"), ...args], { stdio: "inherit" });
241
+ if (result.error) fail(`could not start check_build.js: ${result.error.message}`);
242
+ process.exit(result.status ?? 1);
243
+ }
244
+
220
245
  // `command -v` without a shell: look for an executable on PATH.
221
246
  function hasTool(name) {
222
247
  for (const directory of (process.env.PATH ?? "").split(delimiter)) {
@@ -229,13 +254,18 @@ function hasTool(name) {
229
254
  return false;
230
255
  }
231
256
 
232
- const inDocker = (stage) => useDocker && stage.flag !== "run";
257
+ // BUNINU_JS_TOOLS=1: fetch and build run scripts/fetch.js and scripts/build.js
258
+ // in this process instead of the shell scripts, so they need no toolchain
259
+ // and no Docker; run is unchanged.
260
+ const jsTools = process.env.BUNINU_JS_TOOLS === "1";
261
+ const usesJs = (stage) => jsTools && stage.flag !== "run";
262
+ const inDocker = (stage) => useDocker && stage.flag !== "run" && !jsTools;
233
263
 
234
264
  function checkTools(selectedStages) {
235
265
  const missing = [];
236
266
  for (const stage of selectedStages) {
237
267
  const arch = stage.flag === "run" ? runArch : buildArch;
238
- for (const tool of inDocker(stage) ? ["docker"] : stage.tools(arch)) {
268
+ for (const tool of inDocker(stage) ? ["docker"] : usesJs(stage) ? [] : stage.tools(arch)) {
239
269
  if (!hasTool(tool)) missing.push(`${tool} (for --${stage.flag})`);
240
270
  }
241
271
  }
@@ -284,7 +314,7 @@ function dockerImage() {
284
314
  // into the bind-mounted checkout stays owned by that user; fakeroot provides
285
315
  // the root ownership the initramfs records.
286
316
  function dockerRun(script, env) {
287
- const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "ALPINE_MIRROR"]
317
+ const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "ALPINE_MIRROR", "BUNINU_JS_UKI", "BUNINU_JS_CPIO"]
288
318
  .filter((name) => env[name] !== undefined)
289
319
  .flatMap((name) => ["--env", `${name}=${env[name]}`]);
290
320
  const user = typeof process.getuid === "function"
@@ -327,12 +357,46 @@ const stageEnv = {
327
357
  ...(real && { REAL_MACHINE: "1" }),
328
358
  };
329
359
 
360
+ // The arch.js config for the JS stages, from the same settings as stageEnv.
361
+ const jsConfig = async (env) => (await import("./scripts/arch.js")).config({
362
+ arch: env.BUNINU_ARCH,
363
+ flavor: env.LINUX_FLAVOR,
364
+ real: env.REAL_MACHINE === "1",
365
+ mirror: env.ALPINE_MIRROR,
366
+ });
367
+
368
+ async function runJs(stage, env) {
369
+ const module = stage.flag === "fetch" ? "scripts/fetch.js" : "scripts/build.js";
370
+ console.log(`\n==> [${stage.flag} js] ${module}`);
371
+ try {
372
+ const cfg = await jsConfig(env);
373
+ if (stage.flag === "fetch") await (await import("./scripts/fetch.js")).fetchAll(cfg);
374
+ else await (await import("./scripts/build.js")).buildAll(cfg);
375
+ } catch (error) {
376
+ fail(`${module}: ${error.message}`);
377
+ }
378
+ }
379
+
330
380
  for (const stage of ordered) {
331
381
  // run-qemu.sh builds a missing vda.img itself, which needs the build
332
- // toolchain; with Docker that has to happen in the container first.
333
- if (stage.flag === "run" && useDocker && !selected.has(stages[1]) &&
382
+ // toolchain; with Docker that has to happen in the container first, and
383
+ // with BUNINU_JS_TOOLS=1 scripts/build.js writes it.
384
+ if (stage.flag === "run" && (useDocker || jsTools) && !selected.has(stages[1]) &&
334
385
  !existsSync(resolve(rootDir, "vda.img"))) {
335
- runScript(stages[1], "build-image.sh", [], { ...stageEnv, BUNINU_ARCH: runArch });
386
+ if (jsTools) {
387
+ console.log("\n==> [build js] scripts/build.js image");
388
+ try {
389
+ (await import("./scripts/build.js")).image(await jsConfig({ ...stageEnv, BUNINU_ARCH: runArch }));
390
+ } catch (error) {
391
+ fail(`scripts/build.js: ${error.message}`);
392
+ }
393
+ } else {
394
+ runScript(stages[1], "build-image.sh", [], { ...stageEnv, BUNINU_ARCH: runArch });
395
+ }
396
+ }
397
+ if (usesJs(stage)) {
398
+ await runJs(stage, stageEnv);
399
+ continue;
336
400
  }
337
401
  for (const script of stage.scripts) {
338
402
  runScript(stage, script, stage.flag === "run" ? passthrough : [], stageEnv);