buninu-linux 0.5.1 → 0.6.0

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/fetch-alpine.sh CHANGED
@@ -3,47 +3,34 @@ set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")"
5
5
 
6
- base=https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64
7
- linux_flavor=${LINUX_FLAVOR:-virt}
8
- linux_package="linux-$linux_flavor-6.18.52-r0.apk"
9
- musl_package=musl-1.2.6-r2.apk
10
- stub_package=systemd-efistub-260.2-r0.apk
11
- if [ "$linux_flavor" = lts ]; then
12
- linux_sha256=bd81f00522a7c7deb96886811a087c453d48530c0c8f742412ad73e24236037c
13
- else
14
- linux_sha256=dff0c365a6d6c0fd015175af08dca0c39048fa39df6ef3150a7ea88d2cfdfa4f
15
- fi
16
-
17
- musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
18
- stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
6
+ . ./scripts/arch.sh
19
7
 
20
- kernel_release="6.18.52-0-$linux_flavor"
21
-
22
- modules_root="initramfs/lib/modules/$kernel_release"
23
- mkdir -p downloads kernel initramfs/lib "$modules_root"
8
+ mkdir -p "$downloads_dir" "$kernel_dir" "$native_dir/lib" "$modules_root"
24
9
 
25
10
  . ./scripts/fetch.sh
26
11
 
27
- fetch "$base/$linux_package" "$linux_package" "$linux_sha256"
28
- fetch "$base/$musl_package" "$musl_package" "$musl_sha256"
29
- fetch "$base/$stub_package" "$stub_package" "$stub_sha256"
12
+ fetch "$alpine_base/$linux_package" "$linux_package" "$linux_sha256"
13
+ fetch "$alpine_base/$musl_package" "$musl_package" "$musl_sha256"
14
+ fetch "$alpine_base/$stub_package" "$stub_package" "$stub_sha256"
30
15
 
31
- extract() { tar --warning=no-unknown-keyword -xOf "downloads/$1" "$2"; }
16
+ extract() { tar --warning=no-unknown-keyword -xOf "$downloads_dir/$1" "$2"; }
32
17
 
33
- extract "$linux_package" "boot/vmlinuz-$linux_flavor" > "kernel/vmlinuz-$linux_flavor"
34
- extract "$musl_package" lib/ld-musl-x86_64.so.1 > initramfs/lib/ld-musl-x86_64.so.1
35
- extract "$stub_package" usr/lib/systemd/boot/efi/linuxx64.efi.stub > kernel/linuxx64.efi.stub
18
+ # On aarch64 vmlinuz is an EFI zboot image: a PE executable that inflates
19
+ # the kernel itself, so the stub can start it exactly like the x86_64 bzImage.
20
+ extract "$linux_package" "boot/vmlinuz-$linux_flavor" > "$kernel_image"
21
+ extract "$musl_package" "lib/$musl_loader" > "$native_dir/lib/$musl_loader"
22
+ extract "$stub_package" "usr/lib/systemd/boot/efi/$efi_stub" > "$kernel_dir/$efi_stub"
36
23
 
37
24
  # Kernel modules. The package's modules.dep says what each one needs, so a
38
25
  # module is requested by name and its dependencies come along: on linux-lts
39
- # virtio_net pulls in virtio, virtio_ring and virtio_pci, which linux-virt
40
- # has built in. The initramfs gets a modules.dep trimmed to the shipped set,
26
+ # virtio_net pulls in virtio and virtio_ring, which linux-virt has built in. The initramfs gets a modules.dep trimmed to the shipped set,
41
27
  # the fs-* lines of modules.alias and modules.builtin; /lib/modprobe.js reads
42
28
  # those three files.
43
29
  depfile="$(mktemp)"
44
30
  selected="$(mktemp)"
45
31
  trap 'rm -f "$depfile" "$selected" "$selected.names"' EXIT
46
32
  extract "$linux_package" "lib/modules/$kernel_release/modules.dep" > "$depfile"
33
+ extract "$linux_package" "lib/modules/$kernel_release/modules.builtin" > "$modules_root/modules.builtin"
47
34
 
48
35
  select_module() {
49
36
  local name=$1 line path dep
@@ -58,20 +45,42 @@ select_module() {
58
45
  done
59
46
  }
60
47
 
61
- # Network: virtio-net for QEMU (init.js loads these at boot).
48
+ # For modules one architecture or flavor builds in or leaves out: a built-in
49
+ # one needs nothing, a missing one gets a note. aarch64 linux-virt has no
50
+ # ext2.ko: it sets CONFIG_EXT4_USE_FOR_EXT2, and modules.alias maps fs-ext2 to
51
+ # ext4, which is how /bin/mount looks the driver up.
52
+ select_optional_module() {
53
+ if grep -qF -m1 -- "/$1.ko.gz:" "$depfile"; then
54
+ select_module "$1"
55
+ elif ! grep -q -- "/$1\.ko$" "$modules_root/modules.builtin"; then
56
+ echo "note: module $1 is not in $arch $linux_package; skipped" >&2
57
+ fi
58
+ }
59
+
60
+ # Network: virtio-net for QEMU (init.js loads these at boot). virtio_pci is
61
+ # the PCI transport for every virtio device, disk included; linux-virt builds
62
+ # it in, linux-lts has it as a module that nothing in modules.dep pulls in.
62
63
  for module in failover net_failover virtio_net; do select_module "$module"; done
64
+ select_optional_module virtio_pci
63
65
  # Storage: what /bin/mount loads on demand. ext4 brings jbd2, mbcache and
64
66
  # crc16; FAT, exFAT and NTFS ask the kernel for the NLS tables at mount
65
67
  # time, which has no /sbin/modprobe to answer, so mount preloads them.
66
- for module in virtio_blk ext4 ext2 vfat exfat ntfs3 nls_utf8 nls_cp437 nls_iso8859-1; do
68
+ for module in virtio_blk ext4 vfat exfat ntfs3 nls_utf8 nls_cp437 nls_iso8859-1; do
67
69
  select_module "$module"
68
70
  done
71
+ select_optional_module ext2
69
72
  # Pointing devices for `bunterm --mouse`, which reads /dev/input/event*.
70
73
  # evdev is the character-device interface to the input layer; psmouse drives
71
- # the PS/2 mouse every PC and QEMU's q35 machine has. A USB mouse needs the
72
- # usbhid stack, which only the --real image carries. init.js loads these two
73
- # at boot; nothing reads a pointer unless bunterm is asked to.
74
- for module in evdev psmouse; do select_module "$module"; done
74
+ # the PS/2 mouse every PC and QEMU's q35 machine has. QEMU's aarch64 virt
75
+ # machine has no PS/2 port; its keyboard and pointer are virtio-input. A USB
76
+ # mouse needs the usbhid stack, which only the --real image carries. init.js
77
+ # loads these at boot; nothing reads a pointer unless bunterm is asked to.
78
+ select_module evdev
79
+ if [ "$arch" = x86_64 ]; then
80
+ select_module psmouse
81
+ else
82
+ select_module virtio_input
83
+ fi
75
84
 
76
85
  if [ "${REAL_MACHINE:-}" = 1 ]; then
77
86
  # USB keyboard for the console; common storage paths (SATA/AHCI HDD and
@@ -114,8 +123,7 @@ extract "$linux_package" "lib/modules/$kernel_release/modules.alias" \
114
123
  $2 ~ /^fs-/ { print; next }
115
124
  $2 ~ /^(pci|usb|virtio):/ { m = $3; gsub("-", "_", m); if (m in shipped) print }
116
125
  ' > "$modules_root/modules.alias"
117
- extract "$linux_package" "lib/modules/$kernel_release/modules.builtin" > "$modules_root/modules.builtin"
118
126
 
119
- chmod 0755 initramfs/lib/ld-musl-x86_64.so.1
127
+ chmod 0755 "$native_dir/lib/$musl_loader"
120
128
 
121
- echo "Fetched Alpine linux-$linux_flavor kernel, $(wc -l < "$selected") modules, musl, and the x64 UKI stub."
129
+ echo "Fetched Alpine $arch linux-$linux_flavor kernel, $(wc -l < "$selected") modules, musl, and the $efi_stub UKI stub."
package/fetch-bun.sh CHANGED
@@ -3,17 +3,10 @@ set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")"
5
5
 
6
- bun_version=1.4.2
7
- alpine_base=https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64
8
- bun_url="https://github.com/oven-sh/bun/releases/download/bun-v${bun_version}/bun-linux-x64-musl-baseline.zip"
9
- bun_zip="bun-linux-x64-musl-baseline-${bun_version}.zip"
10
- libstdcpp_package=libstdc++-15.2.0-r5.apk
11
- libgcc_package=libgcc-15.2.0-r5.apk
12
- bun_sha256=76e1db84e98f22f78de0a87e309bfbbf297732847f9720db36750646c85c8c18
13
- libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
14
- libgcc_sha256=393dcd32629f06d7d85409c272d142d0c082772d10b87ef55ee82f47de3be637
15
-
16
- mkdir -p downloads initramfs/bin initramfs/lib initramfs/dev initramfs/proc initramfs/sys initramfs/tmp
6
+ . ./scripts/arch.sh
7
+
8
+ mkdir -p "$downloads_dir" "$native_dir/bin" "$native_dir/lib" \
9
+ initramfs/dev initramfs/proc initramfs/sys initramfs/tmp
17
10
 
18
11
  . ./scripts/fetch.sh
19
12
 
@@ -21,21 +14,21 @@ fetch "$bun_url" "$bun_zip" "$bun_sha256"
21
14
  fetch "$alpine_base/$libstdcpp_package" "$libstdcpp_package" "$libstdcpp_sha256"
22
15
  fetch "$alpine_base/$libgcc_package" "$libgcc_package" "$libgcc_sha256"
23
16
 
24
- test -s initramfs/lib/ld-musl-x86_64.so.1 || {
17
+ test -s "$native_dir/lib/$musl_loader" || {
25
18
  echo "error: fetch stage incomplete: run ./fetch-alpine.sh first (./index.js -f runs both)" >&2
26
19
  exit 1
27
20
  }
28
21
 
29
- unzip -p "downloads/$bun_zip" \
30
- 'bun-linux-x64-musl-baseline/bun' > initramfs/bin/bun
31
- tar --warning=no-unknown-keyword -xOf "downloads/$libstdcpp_package" \
32
- usr/lib/libstdc++.so.6.0.34 > initramfs/lib/libstdc++.so.6
33
- tar --warning=no-unknown-keyword -xOf "downloads/$libgcc_package" \
34
- usr/lib/libgcc_s.so.1 > initramfs/lib/libgcc_s.so.1
22
+ unzip -p "$downloads_dir/$bun_zip" \
23
+ "$bun_build/bun" > "$native_dir/bin/bun"
24
+ tar --warning=no-unknown-keyword -xOf "$downloads_dir/$libstdcpp_package" \
25
+ usr/lib/libstdc++.so.6.0.34 > "$native_dir/lib/libstdc++.so.6"
26
+ tar --warning=no-unknown-keyword -xOf "$downloads_dir/$libgcc_package" \
27
+ usr/lib/libgcc_s.so.1 > "$native_dir/lib/libgcc_s.so.1"
35
28
 
36
29
  # Keep libc as a distinct inode from the active dynamic loader. Calling dlopen
37
30
  # on the loader inode itself deadlocks, while this physical copy is FFI-safe.
38
- cp initramfs/lib/ld-musl-x86_64.so.1 initramfs/lib/libc.musl-x86_64.so.1
39
- chmod 0755 initramfs/init.js initramfs/bin/bun initramfs/lib/*.so*
31
+ cp "$native_dir/lib/$musl_loader" "$native_dir/lib/$musl_libc"
32
+ chmod 0755 initramfs/init.js "$native_dir/bin/bun" "$native_dir"/lib/*.so*
40
33
 
41
- echo "Fetched Bun $bun_version and its x86_64 musl runtime into initramfs/."
34
+ echo "Fetched Bun $bun_version and its $arch musl runtime into $native_dir/."
package/index.js CHANGED
@@ -8,28 +8,51 @@
8
8
 
9
9
  import pkg from "./package.json" with { type: "json" };
10
10
  import { spawnSync } from "node:child_process";
11
- import { accessSync, constants, copyFileSync, readFileSync } from "node:fs";
11
+ import { createHash } from "node:crypto";
12
+ import { accessSync, constants, copyFileSync, existsSync, readFileSync } from "node:fs";
12
13
  import { delimiter, dirname, join, resolve } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
15
 
15
16
  const rootDir = dirname(fileURLToPath(import.meta.url));
16
17
  const invocationDir = process.cwd();
17
18
 
19
+ // Guest architectures. The names are Alpine's and the kernel's; the aliases
20
+ // are what Go, Docker, Node and Apple call the same CPUs. scripts/arch.sh
21
+ // holds everything else that differs between them.
22
+ const architectures = {
23
+ x86_64: { aliases: ["amd64", "x64"], efi: "BOOTX64.EFI", binutils: "x86_64-w64-mingw32", apt: "binutils-mingw-w64-x86-64" },
24
+ aarch64: { aliases: ["arm64"], efi: "BOOTAA64.EFI", binutils: "aarch64-linux-gnu", apt: "binutils-aarch64-linux-gnu" },
25
+ };
26
+
27
+ function normalizeArch(name) {
28
+ for (const [arch, { aliases }] of Object.entries(architectures)) {
29
+ if (name === arch || aliases.includes(name)) return arch;
30
+ }
31
+ fail(`unknown architecture ${name} (use x86_64/amd64 or aarch64/arm64)`);
32
+ }
33
+
34
+ // What vda/EFI/BOOT holds decides the guest for a bare -r, as in run-qemu.sh.
35
+ function builtArch() {
36
+ return existsSync(resolve(rootDir, "vda/EFI/BOOT", architectures.aarch64.efi)) ? "aarch64" : "x86_64";
37
+ }
38
+
18
39
  // Pipeline order. Flags select stages; this array fixes the order they run in.
40
+ // The fetch and build stages can run inside the Docker image from ./Dockerfile
41
+ // (--docker, and always on macOS); run always uses the host's QEMU.
19
42
  const stages = [
20
43
  {
21
44
  flag: "fetch",
22
45
  short: "f",
23
46
  scripts: ["fetch-alpine.sh", "fetch-bun.sh"],
24
- tools: ["curl", "unzip", "tar", "gzip", "sha256sum"],
47
+ tools: () => ["curl", "unzip", "tar", "gzip", "sha256sum"],
25
48
  },
26
49
  {
27
50
  flag: "build",
28
51
  short: "b",
29
52
  scripts: ["build-uki.sh", "build-image.sh"],
30
- tools: [
53
+ tools: (arch) => [
31
54
  "fakeroot", "cpio", "gzip",
32
- "x86_64-w64-mingw32-objcopy", "x86_64-w64-mingw32-objdump",
55
+ `${architectures[arch].binutils}-objcopy`, `${architectures[arch].binutils}-objdump`,
33
56
  "parted", "mkfs.fat", "mmd", "mcopy",
34
57
  ],
35
58
  },
@@ -37,7 +60,7 @@ const stages = [
37
60
  flag: "run",
38
61
  short: "r",
39
62
  scripts: ["run-qemu.sh"],
40
- tools: ["qemu-system-x86_64"],
63
+ tools: (arch) => [`qemu-system-${arch}`],
41
64
  },
42
65
  ];
43
66
 
@@ -50,22 +73,32 @@ function usage() {
50
73
  return `${pkg.name} - ${pkg.description}
51
74
 
52
75
  Usage:
53
- ${pkg.name} [-f] [-b] [-r] [--export] [--linux-lts] [--real] [-- qemu arguments]
76
+ ${pkg.name} [-f] [-b] [-r] [--arch ARCH] [--docker] [--export] [--linux-lts]
77
+ [--real] [-- qemu arguments]
54
78
 
55
79
  Stages (always run in this order, whichever you pick):
56
80
  -f, --fetch download and verify the pinned kernel, musl, GCC runtime,
57
81
  EFI stub and Bun (fetch-alpine.sh, fetch-bun.sh)
58
82
  -b, --build pack the initramfs, assemble the UKI, write vda.img
59
83
  (build-uki.sh, build-image.sh)
60
- -r, --run boot vda.img under QEMU (run-qemu.sh)
84
+ -r, --run boot vda.img under QEMU (run-qemu.sh), with KVM or
85
+ Hypervisor.framework when the guest matches the host CPU
86
+
87
+ --arch ARCH guest architecture: x86_64 (default; alias amd64, x64) or
88
+ aarch64 (alias arm64, native speed on Apple Silicon). A bare
89
+ -r boots whichever one vda.img was last built for
90
+ --docker run fetch and build inside a Debian container built from
91
+ ./Dockerfile; always on for macOS, which lacks the toolchain
61
92
 
62
93
  --export after -b/--build, copy the image to
63
- ./buninu-linux-${pkg.version}.img
94
+ ./buninu-linux-${pkg.version}.img (x86_64) or
95
+ ./buninu-linux-${pkg.version}-aarch64.img
64
96
 
65
97
  --linux-lts use Alpine's general-purpose linux-lts kernel for fetch/build
66
98
  (the default is the smaller linux-virt kernel)
67
99
  --real build for a physical machine: use linux-lts, make tty0 the
68
100
  primary console, and include USB xHCI/HID keyboard modules
101
+ (x86_64 only)
69
102
 
70
103
  -h, --help show this
71
104
  -V, --version show name, version, runtime and platform
@@ -73,11 +106,13 @@ Stages (always run in this order, whichever you pick):
73
106
 
74
107
  Short flags combine: -fbr is the whole pipeline, -br is what an edit to
75
108
  initramfs/init.js needs, -r just boots what is already there. Anything after
76
- -- goes to qemu-system-x86_64 verbatim (for example: -r -- -m 1G).
109
+ -- goes to qemu-system-x86_64 or qemu-system-aarch64 verbatim (for example:
110
+ -r -- -smp 4).
77
111
 
78
- Fetch and build need the PRoot toolchain from README section 0; run needs
79
- qemu-system-x86_64, which Termux provides. Tools missing for the stages you
80
- picked are reported before anything starts.
112
+ Fetch and build need the Debian toolchain from README "Build environment"
113
+ (PRoot, a Debian or Ubuntu host, or --docker); run needs QEMU (Termux
114
+ pkg, apt, or brew install qemu). Tools missing for the stages you picked are
115
+ reported before anything starts.
81
116
  `;
82
117
  }
83
118
 
@@ -87,13 +122,24 @@ function parse(argv) {
87
122
  let linuxLts = false;
88
123
  let real = false;
89
124
  let exportImage = false;
125
+ let docker = false;
126
+ // BUNINU_ARCH is what scripts/arch.sh reads; --arch overrides it.
127
+ let arch = process.env.BUNINU_ARCH ? normalizeArch(process.env.BUNINU_ARCH) : null;
90
128
  let sawSeparator = false;
91
129
 
92
- for (const argument of argv) {
130
+ for (let index = 0; index < argv.length; index++) {
131
+ const argument = argv[index];
93
132
  if (sawSeparator) {
94
133
  passthrough.push(argument);
95
134
  } else if (argument === "--") {
96
135
  sawSeparator = true;
136
+ } else if (argument === "--arch") {
137
+ if (index + 1 >= argv.length) fail("--arch needs a value: x86_64 or aarch64");
138
+ arch = normalizeArch(argv[++index]);
139
+ } else if (argument.startsWith("--arch=")) {
140
+ arch = normalizeArch(argument.slice("--arch=".length));
141
+ } else if (argument === "--docker") {
142
+ docker = true;
97
143
  } else if (argument === "-h" || argument === "--help") {
98
144
  console.log(usage());
99
145
  process.exit(0);
@@ -137,7 +183,10 @@ function parse(argv) {
137
183
  if (exportImage && !selected.has(stages[1])) {
138
184
  fail("--export requires -b/--build (use -fb --export for a clean build)");
139
185
  }
140
- return { selected, passthrough, linuxLts, real, exportImage };
186
+ if (real && arch === "aarch64") {
187
+ fail("--real builds an x86_64 PC image; it cannot be combined with --arch aarch64");
188
+ }
189
+ return { selected, passthrough, linuxLts, real, exportImage, docker, arch };
141
190
  }
142
191
 
143
192
  // Same shape as Buninu's --version. The runtime line tells you whether this
@@ -180,54 +229,120 @@ function hasTool(name) {
180
229
  return false;
181
230
  }
182
231
 
232
+ const inDocker = (stage) => useDocker && stage.flag !== "run";
233
+
183
234
  function checkTools(selectedStages) {
184
235
  const missing = [];
185
236
  for (const stage of selectedStages) {
186
- for (const tool of stage.tools) {
237
+ const arch = stage.flag === "run" ? runArch : buildArch;
238
+ for (const tool of inDocker(stage) ? ["docker"] : stage.tools(arch)) {
187
239
  if (!hasTool(tool)) missing.push(`${tool} (for --${stage.flag})`);
188
240
  }
189
241
  }
190
242
  if (missing.length > 0) {
191
- fail(
192
- `missing tools, nothing was run:\n ${missing.join("\n ")}\n` +
193
- `
194
- Run this command in Debian first:
195
- apt install binutils-mingw-w64-x86-64 cpio curl dosfstools fakeroot mtools parted unzip
196
- `,
197
- );
243
+ const hint = process.platform === "darwin"
244
+ ? `
245
+ Install Docker Desktop (https://www.docker.com/products/docker-desktop/) for
246
+ -f/-b, and QEMU with its UEFI firmware for -r:
247
+ brew install qemu
248
+ `
249
+ : `
250
+ Run this command in Debian or Ubuntu first, or pass --docker to build in a container:
251
+ apt install ${architectures[buildArch].apt} cpio curl dosfstools fakeroot mtools parted unzip
252
+ QEMU for -r:
253
+ apt install qemu-system ovmf qemu-efi-aarch64
254
+ `;
255
+ fail(`missing tools, nothing was run:\n ${missing.join("\n ")}\n${hint}`);
256
+ }
257
+ if (selectedStages.some(inDocker) && spawnSync("docker", ["info"], { stdio: "ignore" }).status !== 0) {
258
+ fail("docker is installed but its daemon is not answering; start Docker Desktop (or dockerd) and retry");
259
+ }
260
+ }
261
+
262
+ // The build image is tagged with a hash of the Dockerfile, so editing it
263
+ // builds a fresh image and an unchanged one is reused without a rebuild. The
264
+ // Dockerfile goes in on stdin: the build needs no context, and sending the
265
+ // checkout (downloads/ included) would only slow it down.
266
+ let dockerImageTag;
267
+ function dockerImage() {
268
+ if (dockerImageTag) return dockerImageTag;
269
+ const dockerfile = readFileSync(resolve(rootDir, "Dockerfile"));
270
+ const tag = `buninu-linux-build:${createHash("sha256").update(dockerfile).digest("hex").slice(0, 12)}`;
271
+ if (spawnSync("docker", ["image", "inspect", tag], { stdio: "ignore" }).status !== 0) {
272
+ console.log(`\n==> [docker] building ${tag} from Dockerfile`);
273
+ const result = spawnSync("docker", ["build", "--tag", tag, "-"], {
274
+ input: dockerfile,
275
+ stdio: ["pipe", "inherit", "inherit"],
276
+ });
277
+ if (result.error) fail(`could not start docker build: ${result.error.message}`);
278
+ if (result.status !== 0) fail(`docker build exited with status ${result.status}`);
198
279
  }
280
+ return (dockerImageTag = tag);
281
+ }
282
+
283
+ // In Docker the script runs as the invoking user, so everything it writes
284
+ // into the bind-mounted checkout stays owned by that user; fakeroot provides
285
+ // the root ownership the initramfs records.
286
+ function dockerRun(script, env) {
287
+ const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "ALPINE_MIRROR"]
288
+ .filter((name) => env[name] !== undefined)
289
+ .flatMap((name) => ["--env", `${name}=${env[name]}`]);
290
+ const user = typeof process.getuid === "function"
291
+ ? ["--user", `${process.getuid()}:${process.getgid()}`]
292
+ : [];
293
+ return [
294
+ "run", "--rm", ...user,
295
+ "--mount", `type=bind,src=${rootDir},dst=/src`, "--workdir", "/src",
296
+ "--env", "HOME=/tmp", ...passEnv,
297
+ dockerImage(), `./${script}`,
298
+ ];
199
299
  }
200
300
 
201
301
  function runScript(stage, script, args = [], env = process.env) {
202
- const path = resolve(rootDir, script);
203
- console.log(`\n==> [${stage.flag}] ${script}${args.length ? " " + args.join(" ") : ""}`);
204
- const result = spawnSync(path, args, { cwd: rootDir, stdio: "inherit", env });
302
+ const docker = inDocker(stage);
303
+ console.log(
304
+ `\n==> [${stage.flag}${docker ? " in docker" : ""}] ${script}${args.length ? " " + args.join(" ") : ""}`,
305
+ );
306
+ const result = docker
307
+ ? spawnSync("docker", dockerRun(script, env), { stdio: "inherit" })
308
+ : spawnSync(resolve(rootDir, script), args, { cwd: rootDir, stdio: "inherit", env });
205
309
  if (result.error) fail(`could not start ${script}: ${result.error.message}`);
206
310
  if (result.status !== 0) {
207
311
  fail(`${script} exited with status ${result.status ?? "signal " + result.signal}`);
208
312
  }
209
313
  }
210
314
 
211
- const { selected, passthrough, linuxLts, real, exportImage } = parse(process.argv.slice(2));
315
+ const { selected, passthrough, linuxLts, real, exportImage, docker, arch } = parse(process.argv.slice(2));
212
316
  const ordered = stages.filter((stage) => selected.has(stage));
317
+ const useDocker = docker || process.platform === "darwin";
318
+ const buildArch = arch ?? "x86_64";
319
+ // A bare -r after -b boots what -b just built; otherwise what vda.img holds.
320
+ const runArch = arch ?? (selected.has(stages[1]) ? buildArch : builtArch());
213
321
  checkTools(ordered);
214
322
 
323
+ const stageEnv = {
324
+ ...process.env,
325
+ ...(arch && { BUNINU_ARCH: arch }),
326
+ ...((linuxLts || real) && { LINUX_FLAVOR: "lts" }),
327
+ ...(real && { REAL_MACHINE: "1" }),
328
+ };
329
+
215
330
  for (const stage of ordered) {
331
+ // 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]) &&
334
+ !existsSync(resolve(rootDir, "vda.img"))) {
335
+ runScript(stages[1], "build-image.sh", [], { ...stageEnv, BUNINU_ARCH: runArch });
336
+ }
216
337
  for (const script of stage.scripts) {
217
- runScript(
218
- stage,
219
- script,
220
- stage.flag === "run" ? passthrough : [],
221
- linuxLts || real
222
- ? { ...process.env, LINUX_FLAVOR: "lts", ...(real && { REAL_MACHINE: "1" }) }
223
- : process.env,
224
- );
338
+ runScript(stage, script, stage.flag === "run" ? passthrough : [], stageEnv);
225
339
  }
226
340
  }
227
341
 
228
342
  if (exportImage) {
229
343
  const source = resolve(rootDir, "vda.img");
230
- const destination = resolve(invocationDir, `buninu-linux-${pkg.version}.img`);
344
+ const suffix = buildArch === "x86_64" ? "" : `-${buildArch}`;
345
+ const destination = resolve(invocationDir, `buninu-linux-${pkg.version}${suffix}.img`);
231
346
  try {
232
347
  accessSync(source, constants.R_OK);
233
348
  copyFileSync(source, destination);
@@ -8,7 +8,7 @@
8
8
  // /etc/resolv.conf, so `chroot /mnt/rootfs` lands in a usable system. Every
9
9
  // mount it makes is undone when the command exits.
10
10
 
11
- import { existsSync, mkdirSync, realpathSync, statSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { chmodSync, existsSync, mkdirSync, realpathSync, statSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { libc, cString, ptr, errno, strerror, Errno } from "../lib/dlopen.js";
13
13
  import { showDocument } from "../lib/document.js";
14
14
 
@@ -125,6 +125,9 @@ if (options.mount) {
125
125
  // A bound file, so name resolution inside follows the host's configuration
126
126
  // and nothing inside the root is modified.
127
127
  if (existsSync("/etc/resolv.conf") && existsSync(`${root}/etc`)) {
128
+
129
+ chmodSync(`/etc/resolv.conf`, 0o644);
130
+
128
131
  prepare({ inside: "/etc/resolv.conf", source: "/etc/resolv.conf", type: "none", flags: MS.BIND, file: true });
129
132
  }
130
133
  }
package/initramfs/bin/ip CHANGED
@@ -498,8 +498,9 @@ const commandAddress = (words, options) => {
498
498
  });
499
499
  };
500
500
 
501
- // struct rtentry on x86_64: rt_dst @8, rt_gateway @24, rt_genmask @40 (each a
502
- // 16-byte sockaddr), rt_flags u16 @56, rt_metric i16 @80, rt_dev char* @88.
501
+ // struct rtentry on 64-bit Linux, x86_64 and aarch64 alike: rt_dst @8,
502
+ // rt_gateway @24, rt_genmask @40 (each a 16-byte sockaddr), rt_flags u16 @56,
503
+ // rt_metric i16 @80, rt_dev char* @88.
503
504
  const rtentry = ({ destination, prefix, gateway, device, metric }) => {
504
505
  const route = new Uint8Array(120);
505
506
  const routeView = view(route);
package/initramfs/bin/ps CHANGED
@@ -16,6 +16,9 @@ const ttyName = (number) => {
16
16
  const minor = (device & 0xff) | ((device >>> 12) & 0xfff00);
17
17
  if (major >= 136 && major <= 143) return `pts/${minor}`;
18
18
  if (major === 4 && minor < 64) return `tty${minor}`;
19
+ // Serial ports: the 8250 UARTs of a PC and the PL011 of QEMU's aarch64 virt.
20
+ if (major === 4) return `ttyS${minor - 64}`;
21
+ if (major === 204 && minor >= 64) return `ttyAMA${minor - 64}`;
19
22
  if (major === 5 && minor === 0) return "tty";
20
23
  return `${major}:${minor}`;
21
24
  };
package/initramfs/bin/tar CHANGED
@@ -41,6 +41,14 @@ for (let index = 0; index < argv.length; index++) {
41
41
  }
42
42
  if (argument === "--zstd") { options.compression = "zstd"; continue; }
43
43
  if (argument === "--gzip") { options.compression = "gzip"; continue; }
44
+ // GNU tar compatibility used by js-udocker. Bun.Archive never restores
45
+ // ownership or permissions, and extraction replaces existing files, so
46
+ // these options already describe its normal extraction behaviour.
47
+ if (argument === "--no-same-owner" || argument === "--no-same-permissions" || argument === "--overwrite") continue;
48
+ // Bun.Archive does not extract device nodes. js-udocker separately removes
49
+ // whiteout files after extraction, so its excludes are safe no-ops here.
50
+ if (argument === "--exclude") { take(index++, argument); continue; }
51
+ if (argument.startsWith("--exclude=")) continue;
44
52
  if (argument === "-C" || argument === "--directory") {
45
53
  options.directory = take(index++, argument);
46
54
  continue;
@@ -67,6 +75,7 @@ for (let index = 0; index < argv.length; index++) {
67
75
  } else if (flag === "r") {
68
76
  fail("-r append is not supported by Bun.Archive");
69
77
  } else if (flag === "v") options.verbose = true;
78
+ else if (flag === "o") {} // GNU tar: --no-same-owner
70
79
  else if (flag === "z") options.compression = "gzip";
71
80
  else if (flag === "f") {
72
81
  const attached = flags.slice(offset + 1);