buninu-linux 0.7.5 → 0.8.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/Dockerfile +5 -2
  2. package/NOTICE.md +110 -49
  3. package/README.md +439 -144
  4. package/fetch-npm.sh +44 -0
  5. package/fetch-plus-build.sh +1 -0
  6. package/index.js +15 -13
  7. package/initramfs/bin/buninu-browser +24 -0
  8. package/initramfs/bin/jspinyin +12 -0
  9. package/initramfs/bin/jspulse +2 -0
  10. package/initramfs/bin/tar +40 -6
  11. package/initramfs/bin/top +1 -7
  12. package/initramfs/buninu/CHANGELOG.md +4 -0
  13. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +28 -0
  14. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  15. package/initramfs/buninu/apps/bunmsh/src/main.js +7 -5
  16. package/initramfs/buninu/apps/bunmsh/src/shell.js +584 -75
  17. package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +64 -0
  18. package/initramfs/buninu/package.json +1 -1
  19. package/initramfs/init.js +5 -0
  20. package/initramfs/lib/browser-resources.js +25 -0
  21. package/initramfs/lib/tar-exclude.js +20 -0
  22. package/initramfs/lib/tar-strip.js +94 -0
  23. package/initramfs/lib/top-memory.js +12 -0
  24. package/initramfs/usr/share/doc/buninu-linux/tar.md +24 -1
  25. package/initramfs/usr/share/doc/buninu-linux/top.md +18 -0
  26. package/package.json +1 -1
  27. package/scripts/arch.js +5 -5
  28. package/scripts/arch.sh +5 -5
  29. package/scripts/fetch-npm.js +51 -0
  30. package/scripts/fetch.js +1 -0
  31. package/scripts/js-tools.js +3 -0
  32. package/scripts/npm-packages.md +18 -0
  33. package/scripts/pack-initramfs.sh +1 -0
  34. package/scripts/pack.js +2 -0
  35. package/scripts/stage-npm.js +57 -0
  36. package/scripts/stage-npm.sh +62 -0
  37. package/test/fetch-npm.test.js +92 -0
  38. package/test/host/Dockerfile +6 -4
  39. package/test/host/boot-test.exp +2 -2
  40. package/test/host/host-test.sh +2 -2
  41. package/test/host/in-container.sh +4 -4
  42. package/test/js-tools-selection.test.js +19 -0
  43. package/test/npm-launchers.test.js +59 -0
  44. package/test/stage-npm.test.js +130 -0
  45. package/test/tar-strip.test.js +218 -0
  46. package/test/top-memory.test.js +23 -0
  47. package/initramfs/buninu/apps/bunmsh/test/shell.test.js +0 -2092
@@ -420,6 +420,69 @@ describe("execution", () => {
420
420
  });
421
421
  });
422
422
 
423
+ test("parses POSIX case clauses independently of physical lines", async () => {
424
+ expect(await run('case "a b" in a\\ b|no) echo escaped;; *) echo bad;; esac')).toMatchObject({
425
+ status: 0,
426
+ stdout: "escaped\n",
427
+ stderr: "",
428
+ });
429
+ expect(await run("case x in (no) echo bad ;; (x) echo parenthesized\nesac")).toMatchObject({
430
+ status: 0,
431
+ stdout: "parenthesized\n",
432
+ stderr: "",
433
+ });
434
+ expect(await run("case x in x) case y in y) echo nested;; esac ;; *) echo bad;; esac")).toMatchObject({
435
+ status: 0,
436
+ stdout: "nested\n",
437
+ stderr: "",
438
+ });
439
+ expect(await run("case outer in outer) case inner in inner) echo autoconf;; esac;; *) echo bad;; esac")).toMatchObject({
440
+ status: 0,
441
+ stdout: "autoconf\n",
442
+ stderr: "",
443
+ });
444
+ expect(await run("case x in x) echo case;; esac; echo after")).toMatchObject({
445
+ status: 0,
446
+ stdout: "case\nafter\n",
447
+ stderr: "",
448
+ });
449
+ expect(await run("case b in [!ac]) echo bracket;; *) echo bad;; esac; case '*' in '*') echo quoted;; esac")).toMatchObject({
450
+ stdout: "bracket\nquoted\n",
451
+ });
452
+ });
453
+
454
+ test("implements mksh case fall-through terminators", async () => {
455
+ expect(await run("case b in a) echo a ;& b) echo b ;& c) echo c ;; esac")).toMatchObject({
456
+ stdout: "b\nc\n",
457
+ });
458
+ expect(await run("case b in a) echo a ;| b) echo b ;| *) echo star ;; esac")).toMatchObject({
459
+ stdout: "b\nstar\n",
460
+ });
461
+ });
462
+
463
+ test("accepts POSIX for headers with do on the next line", async () => {
464
+ expect(await run("for value in one two\ndo\n echo $value\ndone")).toMatchObject({
465
+ status: 0,
466
+ stdout: "one\ntwo\n",
467
+ stderr: "",
468
+ });
469
+ expect(await run("set -- left right; for arg\ndo echo $arg; done")).toMatchObject({
470
+ status: 0,
471
+ stdout: "left\nright\n",
472
+ stderr: "",
473
+ });
474
+ expect(await run("set -- a b; for arg; do printf '<%s>\\n' \"$arg\"; done")).toMatchObject({
475
+ status: 0,
476
+ stdout: "<a>\n<b>\n",
477
+ stderr: "",
478
+ });
479
+ expect(await run("for value in; do echo bad; done; echo empty")).toMatchObject({
480
+ status: 0,
481
+ stdout: "empty\n",
482
+ stderr: "",
483
+ });
484
+ });
485
+
423
486
  test("provides basic fallback text and utility commands", async () => {
424
487
  expect(await run("printf 'c\\na\\nb\\n' | builtin sort")).toMatchObject({ stdout: "a\nb\nc\n" });
425
488
  expect(await run("printf 'one two\\nthree\\n' | builtin wc -lwc")).toMatchObject({ stdout: "2 3 14\n" });
@@ -933,6 +996,7 @@ fi
933
996
  test("expands variables while respecting single quotes", async () => {
934
997
  const output = await run(`X=world; print -r -- "hello $X"; print -r -- '$X'`);
935
998
  expect(output.stdout).toBe("hello world\n$X\n");
999
+ expect((await run("printf '%s\\n' \"literal\\`backtick\"")).stdout).toBe("literal`backtick\n");
936
1000
  });
937
1001
 
938
1002
  test("supports status connectors", async () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
5
5
  "type": "module",
6
6
  "main": "./bin/init.js",
package/initramfs/init.js CHANGED
@@ -435,6 +435,11 @@ const launchBunmsh = () => {
435
435
  let replServer = null;
436
436
 
437
437
  const launchBuninu = async () => {
438
+ try {
439
+ await Bun.write("/proc/sys/kernel/hostname", "buninu\n");
440
+ } catch (error) {
441
+ console.error(`init.js: setting hostname: ${error?.message ?? error}`);
442
+ }
438
443
 
439
444
  console.log(`
440
445
  Type bunterm in bunmsh: Graphical Terminal
@@ -0,0 +1,25 @@
1
+ // Distribution-owned startup hook: npm updates may replace the browser, but
2
+ // cannot replace this helper. Existing package resources are never overwritten.
3
+ import { lstatSync, mkdirSync, statSync, symlinkSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+
6
+ const lstat = (path) => {
7
+ try { return lstatSync(path); }
8
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
9
+ };
10
+
11
+ export function ensureBrowserResources(browser, systemRoot = "/") {
12
+ const resources = [
13
+ ["usr/lib/canvaskit", "lib/canvaskit"],
14
+ ["usr/share/fonts", "usr/share/fonts"],
15
+ ];
16
+ for (const [local, shared] of resources) {
17
+ const path = join(browser, local);
18
+ if (lstat(path)) continue; // Includes symlinks: do not overwrite user data.
19
+ const target = resolve(systemRoot, shared);
20
+ if (!statSync(target).isDirectory()) throw new Error(`missing browser resources: ${target}`);
21
+ mkdirSync(dirname(path), { recursive: true });
22
+ try { symlinkSync(relative(dirname(path), target), path); }
23
+ catch (error) { if (error.code !== "EEXIST") throw error; }
24
+ }
25
+ }
@@ -0,0 +1,20 @@
1
+ // Match original archive paths, before --strip-components. Testing ancestors
2
+ // makes an excluded directory exclude its whole subtree, even without a
3
+ // directory header. Like tar's default excludes, matches may start at any
4
+ // path component. Glob syntax is Bun.Glob's, not every GNU tar extension.
5
+ const normalize = (name) => name.replace(/^(?:\.\/)+/, "").replace(/^\/+|\/+$/g, "");
6
+
7
+ export function excludeMatcher(patterns) {
8
+ if (!patterns.length) return () => false;
9
+ const globs = patterns.map((pattern) => new Bun.Glob(normalize(pattern)));
10
+ return (name) => {
11
+ const parts = normalize(name).split("/");
12
+ for (let end = parts.length; end > 0; end--) {
13
+ for (let start = 0; start < end; start++) {
14
+ const candidate = parts.slice(start, end).join("/");
15
+ if (globs.some((glob) => glob.match(candidate))) return true;
16
+ }
17
+ }
18
+ return false;
19
+ };
20
+ }
@@ -0,0 +1,94 @@
1
+ // Opt-in --strip-components / --exclude extraction (count may be zero).
2
+ // The ordinary Bun.Archive path is
3
+ // deliberately separate. Payload offsets come from the CLI's tar scanner.
4
+ import { linkSync, lstatSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+
7
+ export function strippedName(name, count) {
8
+ const parts = name.replace(/^\/+/, "").split("/").filter(Boolean);
9
+ // Validate before stripping so removed prefixes cannot hide traversal.
10
+ if (parts.includes("..") || name.includes("\0")) throw new Error(`unsafe archive path: ${name}`);
11
+ // A leading './' counts as a component, as in GNU tar. Interior '.'
12
+ // components can be removed after counting to form the destination path.
13
+ const remaining = parts.slice(count).filter((part) => part !== ".");
14
+ return remaining.length ? remaining.join("/") : null;
15
+ }
16
+
17
+ const stat = (path) => {
18
+ try { return lstatSync(path); }
19
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
20
+ };
21
+
22
+ export function extractStripped(bytes, entries, directory, count, { verbose = false, filtered = false } = {}) {
23
+ mkdirSync(directory, { recursive: true });
24
+ const root = realpathSync(directory);
25
+ // Refuse existing symlink ancestors too, not just links in this archive.
26
+ const ensureDirectory = (name) => {
27
+ let path = root;
28
+ for (const part of name.split("/")) {
29
+ if (!part || part === ".") continue;
30
+ path = join(path, part);
31
+ const info = stat(path);
32
+ if (info && (!info.isDirectory() || info.isSymbolicLink())) throw new Error(`not a safe directory: ${path}`);
33
+ if (!info) mkdirSync(path);
34
+ }
35
+ return path;
36
+ };
37
+ const destination = (name) => {
38
+ ensureDirectory(dirname(name));
39
+ return join(root, name);
40
+ };
41
+ const removeLeaf = (path) => {
42
+ if (stat(path)) rmSync(path); // Never recursively remove directories.
43
+ };
44
+ const hardlinks = [], symlinks = [];
45
+ for (const entry of entries) {
46
+ const name = strippedName(entry.archiveName ?? entry.name, count);
47
+ if (!name) continue;
48
+ if (verbose) console.log(entry.name);
49
+ if (entry.kind === "directory") {
50
+ ensureDirectory(name);
51
+ } else if (entry.kind === "file") {
52
+ const { dataOffset: start, size } = entry;
53
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || size < 0
54
+ || start < 0 || size > bytes.length - start) throw new Error(`truncated tar member: ${entry.name}`);
55
+ const path = destination(name);
56
+ removeLeaf(path);
57
+ writeFileSync(path, bytes.subarray(start, start + size), { flag: "wx" });
58
+ } else if (entry.kind === "hardlink") {
59
+ const target = strippedName(entry.target, count);
60
+ if (target) hardlinks.push({ name, target });
61
+ } else if (entry.kind === "symlink") {
62
+ symlinks.push({ name, target: entry.target });
63
+ }
64
+ // Device nodes and FIFOs remain unsupported, like Bun.Archive.
65
+ }
66
+ // Hardlink targets are archive-root-relative and must also be stripped.
67
+ // Retry forward references and chains before making any symbolic links.
68
+ let pending = hardlinks;
69
+ while (pending.length) {
70
+ const next = [];
71
+ for (const entry of pending) {
72
+ const source = destination(entry.target);
73
+ const info = stat(source);
74
+ if (!info) { next.push(entry); continue; }
75
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error(`unsafe hardlink target: ${entry.target}`);
76
+ const path = destination(entry.name);
77
+ if (path === source) continue;
78
+ removeLeaf(path);
79
+ linkSync(source, path);
80
+ }
81
+ if (next.length === pending.length) {
82
+ if (!filtered) throw new Error(`missing hardlink target: ${next[0].target}`);
83
+ break;
84
+ }
85
+ pending = next;
86
+ }
87
+ // Symbolic link contents are not member names: preserve them verbatim,
88
+ // including absolute rootfs targets. No file data is written afterwards.
89
+ for (const entry of symlinks) {
90
+ const path = destination(entry.name);
91
+ removeLeaf(path);
92
+ symlinkSync(entry.target, path);
93
+ }
94
+ }
@@ -0,0 +1,12 @@
1
+ // /proc/meminfo reports KiB. Cache is informational, not all reclaimable.
2
+ export function memoryText(memory) {
3
+ const total = memory.MemTotal ?? 0;
4
+ const free = memory.MemFree ?? 0;
5
+ const available = memory.MemAvailable;
6
+ // Old kernels without MemAvailable: conservatively count everything not
7
+ // free as used, and do not present that fallback as a kernel estimate.
8
+ const used = Math.max(0, total - (available ?? free));
9
+ const cache = (memory.Buffers ?? 0) + (memory.Cached ?? 0) + (memory.SReclaimable ?? 0);
10
+ const mib = (value) => (value / 1024).toFixed(1);
11
+ return `MiB Mem : ${mib(total)} total, ${mib(free)} free, ${mib(used)} used, ${mib(cache)} buff/cache, ${available == null ? "n/a" : mib(available)} available`;
12
+ }
@@ -25,11 +25,12 @@ Use `-` as ARCHIVE to read from standard input or write to standard output.
25
25
  | `-t` | List archive members. |
26
26
  | `-f ARCHIVE`, `--file=ARCHIVE` | Archive file, or `-` for standard input/output. |
27
27
  | `-C DIR`, `--directory=DIR` | Read input files from DIR, or extract into DIR. |
28
+ | `--strip-components=N`, `--strip-components N` | Remove N leading path components when extracting. N must be a non-negative integer. Members with no remaining name are skipped. |
28
29
  | `-z`, `--gzip` | Create gzip-compressed tar, or explicitly identify gzip input. |
29
30
  | `--zstd` | Create or read a zstd-compressed tar. A `.zst` or `.tzst` input is also detected by name. |
30
31
  | `-v` | Print names while creating/extracting; with `-t`, also show each member's size, modification time, kind and link target. |
31
32
  | `-o`, `--no-same-owner`, `--no-same-permissions`, `--overwrite` | GNU tar compatibility for extraction; these already match Bun.Archive's behaviour. |
32
- | `--exclude=PATTERN` | Accepted for js-udocker compatibility. Device nodes are not extracted and js-udocker removes whiteout files separately. |
33
+ | `--exclude=PATTERN`, `--exclude PATTERN` | Exclude matching paths and their descendants during creation, listing, or extraction. Repeat to exclude multiple patterns. Uses Bun.Glob syntax; quote wildcards to prevent shell expansion. |
33
34
 
34
35
  Short options can be combined, for example `-xzvf` or `-czf`.
35
36
  For the traditional first option word, the leading hyphen is optional, so
@@ -45,6 +46,12 @@ tar -tf source.tar.gz
45
46
  tar --zstd -cf backup.tar.zst home
46
47
  tar --zstd -xf backup.tar.zst -C /mnt/restore
47
48
 
49
+ # Remove the package/ directory from an npm tarball
50
+ tar -xzf package.tgz -C /tmp/package --strip-components=1
51
+
52
+ # Exclude browser resources; match before removing package/
53
+ tar -xzf buninu-browser-0.2.2.tgz -C /tmp/browser --strip-components=1 --exclude=package/usr
54
+
48
55
  # An Alpine root filesystem, symbolic links and all; then enter it
49
56
  tar -xzf alpine-minirootfs-3.24.1-x86_64.tar.gz -C /mnt/alpine
50
57
  chroot /mnt/alpine
@@ -52,6 +59,22 @@ chroot /mnt/alpine
52
59
 
53
60
  ## Links
54
61
 
62
+ `--strip-components` applies only to extraction. Member filters match the
63
+ original archive names, before stripping; listing still shows original names.
64
+ Hardlink targets have the same number of components removed, while symbolic
65
+ link targets are preserved verbatim. A leading `./` counts as one component.
66
+ Exclude patterns match original paths, ignoring a leading `./` or `/` and
67
+ trailing `/`. They may match starting at any path component; `usr` excludes
68
+ any directory or file named `usr`, while `package/usr` targets that path.
69
+ An excluded directory excludes all descendants, even without a directory
70
+ header in the archive. Excluded hardlink targets are not restored from
71
+ pre-existing destination files; symbolic link targets remain verbatim.
72
+ This is not a complete implementation of GNU tar's pattern options.
73
+
74
+ Without excludes and with no stripping (or `--strip-components=0`), the existing
75
+ `Bun.Archive.extract()` route is unchanged. Positive counts or excludes use a separate
76
+ extractor which rejects `..` components and symlink ancestors in output paths.
77
+
55
78
  Extraction restores symbolic links exactly as the archive records them,
56
79
  including absolute targets such as `/bin/busybox` and targets that do not
57
80
  exist yet, and it restores hard links. This is what GNU tar does, and it is
@@ -21,3 +21,21 @@ While running interactively, press `q` to quit, `P` to sort by CPU, `M` to
21
21
  sort by memory, or `N` to sort by PID. The first screen shows the lifetime CPU
22
22
  average available from `ps`; later screens show usage during the refresh
23
23
  interval.
24
+
25
+ ## Memory summary
26
+
27
+ The summary uses `/proc/meminfo`, converting KiB to MiB:
28
+
29
+ - `total`: `MemTotal`.
30
+ - `free`: `MemFree`, currently unused memory.
31
+ - `used`: `MemTotal - MemAvailable` (clamped at zero).
32
+ - `available`: `MemAvailable`, the kernel's estimate of memory available to
33
+ new applications without swapping.
34
+ - `buff/cache`: `Buffers + Cached + SReclaimable`, shown for reference.
35
+
36
+ Cache is not all immediately reclaimable, particularly with memory-backed
37
+ filesystems. It is no longer subtracted directly to calculate `used`.
38
+ These fields overlap: `used + free + buff/cache` need not equal `total`.
39
+ If a kernel does not report `MemAvailable`, `available` shows `n/a` and
40
+ `used` falls back to `MemTotal - MemFree`, without subtracting cache.
41
+ Per-process memory columns are unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.7.5",
3
+ "version": "0.8.55",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/scripts/arch.js CHANGED
@@ -16,8 +16,8 @@ export const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
16
 
17
17
  const pins = {
18
18
  x86_64: {
19
- linuxVirtSha256: "57a522d2b6e9d1b6c9de6b9f7fc42aa4b8d33ebf9f2b42b4251dbee6ff2d9839",
20
- linuxLtsSha256: "1d02a1b74a8a09c476a3c01782c0b8613ae2935988e65d4d31e6bef44dfb8b64",
19
+ linuxVirtSha256: "a941c15fc5db26b6692fd0140fa0970da76cb12aadf3dc8306c419f21bd39c93",
20
+ linuxLtsSha256: "02ba9491d4cc110707de64ed03dd7e2fa5c93fdd851979c244ceb018785a9eea",
21
21
  muslSha256: "573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71",
22
22
  stubSha256: "8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9",
23
23
  libstdcppSha256: "14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6",
@@ -32,8 +32,8 @@ const pins = {
32
32
  serialMajor: 4,
33
33
  },
34
34
  aarch64: {
35
- linuxVirtSha256: "9a4a6fa042b60b70f453dded99762810516b35793c64d776d75986b0b110b6e0",
36
- linuxLtsSha256: "f945ddcc306546c7c22cf10249f637fc6e806ecc40286fb50950558dd573e7e9",
35
+ linuxVirtSha256: "c7fb892408d7fe163a18671e5c7816752976d1c67fd17794dfba794aa0d6c1ac",
36
+ linuxLtsSha256: "31aeae56fa527b2fb4fdd4aefbd693cbec210a8cbfd4ea7597a1773b832e9f83",
37
37
  muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
38
38
  stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
39
39
  libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
@@ -67,7 +67,7 @@ export const config = ({
67
67
  // The Alpine repository keeps only the newest build of a package, so a pin
68
68
  // stops downloading when Alpine bumps it; the hashes then fail loudly.
69
69
  const alpineBase = `${mirror}/v3.24/main/${arch}`;
70
- const kernelVersion = "6.18.54";
70
+ const kernelVersion = "6.18.55";
71
71
  const kernelRelease = `${kernelVersion}-0-${flavor}`;
72
72
  const bunVersion = "1.4.2";
73
73
  const downloadsDir = join(rootDir, "downloads", arch);
package/scripts/arch.sh CHANGED
@@ -44,7 +44,7 @@ fi
44
44
  alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
45
45
  alpine_base="$alpine_mirror/v3.24/main/$arch"
46
46
 
47
- kernel_version=6.18.54
47
+ kernel_version=6.18.55
48
48
  kernel_release="$kernel_version-0-$linux_flavor"
49
49
  linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
50
50
  musl_package=musl-1.2.6-r2.apk
@@ -63,8 +63,8 @@ musl_libc="libc.musl-$arch.so.1"
63
63
 
64
64
  case $arch in
65
65
  x86_64)
66
- linux_virt_sha256=57a522d2b6e9d1b6c9de6b9f7fc42aa4b8d33ebf9f2b42b4251dbee6ff2d9839
67
- linux_lts_sha256=1d02a1b74a8a09c476a3c01782c0b8613ae2935988e65d4d31e6bef44dfb8b64
66
+ linux_virt_sha256=a941c15fc5db26b6692fd0140fa0970da76cb12aadf3dc8306c419f21bd39c93
67
+ linux_lts_sha256=02ba9491d4cc110707de64ed03dd7e2fa5c93fdd851979c244ceb018785a9eea
68
68
  musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
69
69
  stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
70
70
  libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
@@ -82,8 +82,8 @@ case $arch in
82
82
  serial_major=4
83
83
  ;;
84
84
  aarch64)
85
- linux_virt_sha256=9a4a6fa042b60b70f453dded99762810516b35793c64d776d75986b0b110b6e0
86
- linux_lts_sha256=f945ddcc306546c7c22cf10249f637fc6e806ecc40286fb50950558dd573e7e9
85
+ linux_virt_sha256=c7fb892408d7fe163a18671e5c7816752976d1c67fd17794dfba794aa0d6c1ac
86
+ linux_lts_sha256=31aeae56fa527b2fb4fdd4aefbd693cbec210a8cbfd4ea7597a1773b832e9f83
87
87
  musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
88
88
  stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
89
89
  libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
@@ -0,0 +1,51 @@
1
+ // Fetch only: no package manager, lifecycle scripts, extraction or install
2
+ // paths. Shell and JS use the same pins; both architectures share the cache.
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { rootDir, runIfMain } from "./arch.js";
6
+ import { download } from "./fetch.js";
7
+
8
+ export function parseNpmPins(text) {
9
+ // Only check the fixed shell-compatible shape here (including mandatory
10
+ // double quotes). Values are parsed once below, then validated as data.
11
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() && !line.startsWith("#"));
12
+ const shape = [/^"[^"\\]+":$/, /^ - https:\/\/[^\s"#]+$/, /^ - sha256:[^\s"#]+$/];
13
+ if (!lines.length || lines.length % 3 || lines.some((line, index) => !shape[index % 3].test(line))) {
14
+ throw new Error("invalid npm pin format");
15
+ }
16
+ // Bun uses its native YAML parser. Node's fallback reads only the fixed
17
+ // three-line format, without a YAML dependency or a second parse on Bun.
18
+ const document = globalThis.Bun?.YAML
19
+ ? Bun.YAML.parse(text)
20
+ : Object.fromEntries(lines.flatMap((line, index) => index % 3 ? [] : [
21
+ [line.slice(1, -2), [lines[index + 1].slice(4), lines[index + 2].slice(4)]],
22
+ ]));
23
+ const entries = Object.entries(document);
24
+ if (entries.length !== lines.length / 3) throw new Error("duplicate npm pin");
25
+ const archives = new Set();
26
+ return entries.map(([key, values]) => {
27
+ const header = key.match(/^((?:@[a-z0-9._-]+\/)?[a-z0-9._-]+)@(\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?)$/);
28
+ if (!header || !Array.isArray(values) || values.length !== 2
29
+ || typeof values[0] !== "string" || typeof values[1] !== "string"
30
+ || !/^https:\/\/registry\.npmjs\.org\/[^\s"#]+\.tgz$/.test(values[0])
31
+ || !/^sha256:[a-f0-9]{64}$/.test(values[1])) throw new Error(`invalid npm pin: ${key}`);
32
+ const [, name, version] = header;
33
+ const archive = `${name.replace(/^@/, "").replace("/", "-")}-${version}.tgz`;
34
+ if (archives.has(archive)) throw new Error(`duplicate npm pin: ${name}`);
35
+ archives.add(archive);
36
+ return { name, version, archive, url: values[0], sha256: values[1].slice(7) };
37
+ });
38
+ }
39
+
40
+ export const npmPackages = parseNpmPins(readFileSync(new URL("./npm-packages.md", import.meta.url), "utf8"));
41
+ export const npmDownloadsDir = join(rootDir, "downloads", "npm");
42
+
43
+ export async function fetchNpm({ downloadsDir = npmDownloadsDir, downloadFile = download } = {}) {
44
+ const paths = [];
45
+ for (const pkg of npmPackages) {
46
+ paths.push(await downloadFile({ downloadsDir }, pkg.url, pkg.archive, pkg.sha256));
47
+ }
48
+ return paths;
49
+ }
50
+
51
+ runIfMain(import.meta.url, () => fetchNpm());
package/scripts/fetch.js CHANGED
@@ -189,6 +189,7 @@ export const fetchBun = (cfg = config()) => withUmask022(async () => {
189
189
  export const fetchAll = async (cfg = config()) => {
190
190
  await fetchAlpine(cfg);
191
191
  await fetchBun(cfg);
192
+ await (await import("./fetch-npm.js")).fetchNpm();
192
193
  };
193
194
 
194
195
  runIfMain(import.meta.url, () => fetchAll());
@@ -0,0 +1,3 @@
1
+ // Opt out explicitly; unset, empty and other values keep the JS tools enabled.
2
+ export const jsToolsEnabled = (value = process.env.BUNINU_JS_TOOLS) =>
3
+ !["0", "off", "false"].includes(String(value ?? "").trim().toLowerCase());
@@ -0,0 +1,18 @@
1
+ # npm packages
2
+ # Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
3
+
4
+ "buninu-browser@0.2.2":
5
+ - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.2.tgz
6
+ - sha256:d551fec88f0fe8a6494f806cc8b4b560454796e25f25493e8a3b5b77e6f7f10b
7
+
8
+ "@drxiaozhi/casty@1.3.4":
9
+ - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.4.tgz
10
+ - sha256:1899179d173702721c2f6e8bbffab51ca3421e899ac59c58b2588cfc342fa0df
11
+
12
+ "@drxiaozhi/jspulse@0.2.1":
13
+ - https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.1.tgz
14
+ - sha256:9a34a76b1edac6767fa90e33cbc4d3a511c082e9ab73fb196390f841aad37839
15
+
16
+ "@drxiaozhi/jspinyin@0.1.1":
17
+ - https://registry.npmjs.org/@drxiaozhi/jspinyin/-/jspinyin-0.1.1.tgz
18
+ - sha256:c6d10a40b6c93a942d85dff69ed2e45dacf506b15caab6821688e8e29df810ed
@@ -25,6 +25,7 @@ trap 'rm -rf "$staging"; rm -f "$archive"' EXIT
25
25
  cp -a --reflink=auto initramfs/. "$staging/"
26
26
  rm -rf "$staging/bin/bun" "$staging/lib/modules"
27
27
  cp -a --reflink=auto "$native_dir/." "$staging/"
28
+ bash scripts/stage-npm.sh "$staging"
28
29
  # Only this kernel's modules: native/<arch>/ keeps every release fetched so
29
30
  # far (an older pin, the other flavor), and none of those can load.
30
31
  for release_dir in "$staging/lib/modules"/*; do
package/scripts/pack.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from "node:fs";
26
26
  import { join } from "node:path";
27
27
  import { config, jsRuntime, rootDir, runIfMain } from "./arch.js";
28
+ import { stageNpm } from "./stage-npm.js";
28
29
 
29
30
  /**
30
31
  * `cp -a SOURCE/. TARGET/`: modes, times and symlinks kept, directories
@@ -138,6 +139,7 @@ const packStaging = (cfg) => {
138
139
  rmSync(join(staging, "bin", "bun"), { force: true });
139
140
  rmSync(join(staging, "lib", "modules"), { recursive: true, force: true });
140
141
  copyTree(cfg.nativeDir, staging);
142
+ stageNpm(staging);
141
143
  // Only this kernel's modules: native/<arch>/ keeps every release fetched
142
144
  // so far (an older pin, the other flavor), and none of those can load.
143
145
  const modules = join(staging, "lib", "modules");
@@ -0,0 +1,57 @@
1
+ // Assemble fetched packages in a disposable pack staging tree, never in the
2
+ // source initramfs. No registry access, dependency resolution or lifecycle code.
3
+ import { createHash } from "node:crypto";
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { join, relative, resolve } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { jsRuntime, rootDir, runIfMain } from "./arch.js";
8
+ import { npmDownloadsDir, npmPackages } from "./fetch-npm.js";
9
+
10
+ export function stageNpm(staging, { packages = npmPackages, downloadsDir = npmDownloadsDir } = {}) {
11
+ const lib = resolve(staging, "usr/lib");
12
+ if (existsSync(join(lib, "package.json")) || existsSync(join(lib, "node_modules"))) {
13
+ throw new Error("npm staging destination already exists; refusing to overwrite it");
14
+ }
15
+ // Validate every archive before extracting any of them, including cached data.
16
+ for (const pkg of packages) {
17
+ const archive = join(downloadsDir, pkg.archive);
18
+ if (!existsSync(archive)) throw new Error(`missing ${archive}; run the fetch stage first`);
19
+ if (createHash("sha256").update(readFileSync(archive)).digest("hex") !== pkg.sha256) {
20
+ throw new Error(`SHA-256 mismatch: ${archive}`);
21
+ }
22
+ }
23
+ const runtime = jsRuntime();
24
+ const probe = spawnSync(runtime, ["-e", "process.exit(typeof Bun === 'undefined' ? 1 : 0)"], { stdio: "ignore" });
25
+ if (probe.status !== 0) throw new Error("JS npm extraction requires host Bun on PATH (for initramfs/bin/tar)");
26
+ const bins = join(lib, "node_modules/.bin");
27
+ mkdirSync(bins, { recursive: true });
28
+ for (const pkg of packages) {
29
+ const destination = join(lib, "node_modules", pkg.name);
30
+ mkdirSync(destination, { recursive: true });
31
+ const args = [join(rootDir, "initramfs/bin/tar"), "-xf", join(downloadsDir, pkg.archive),
32
+ "-C", destination, "--strip-components=1"];
33
+ if (pkg.name === "buninu-browser") args.push(
34
+ "--exclude=package/usr/lib/canvaskit", "--exclude=package/usr/share/fonts");
35
+ const result = spawnSync(runtime, args, { stdio: "inherit" });
36
+ if (result.status !== 0) throw new Error(`npm extraction failed: ${pkg.name}`);
37
+ const info = JSON.parse(readFileSync(join(destination, "package.json"), "utf8"));
38
+ if (info.name !== pkg.name || info.version !== pkg.version) throw new Error(`npm identity mismatch: ${pkg.name}`);
39
+ const entries = typeof info.bin === "string" ? { [pkg.name.split("/").pop()]: info.bin } : info.bin ?? {};
40
+ for (const [name, file] of Object.entries(entries)) {
41
+ const target = resolve(destination, file);
42
+ if (!/^[a-zA-Z0-9._-]+$/.test(name) || name === "." || name === ".."
43
+ || !target.startsWith(destination + "/")) throw new Error(`unsafe npm bin: ${name}`);
44
+ chmodSync(target, 0o755);
45
+ symlinkSync(relative(bins, target), join(bins, name));
46
+ }
47
+ }
48
+ writeFileSync(join(lib, "package.json"), JSON.stringify({
49
+ name: "buninu-system-apps", private: true,
50
+ dependencies: Object.fromEntries(packages.map(({ name, version }) => [name, version])),
51
+ }, null, 2) + "\n");
52
+ }
53
+
54
+ runIfMain(import.meta.url, () => {
55
+ if (!process.argv[2]) throw new Error("usage: bun scripts/stage-npm.js STAGING_DIRECTORY");
56
+ stageNpm(process.argv[2]);
57
+ });
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # Shell pack counterpart: the Docker toolchain intentionally needs no host Bun.
3
+ set -euo pipefail
4
+ umask 022
5
+ cd "$(dirname "$0")/.."
6
+ staging=${1:?usage: stage-npm.sh STAGING_DIRECTORY}
7
+ lib="$staging/usr/lib"
8
+ [[ ! -e "$lib/package.json" && ! -e "$lib/node_modules" ]] || {
9
+ echo 'error: npm staging destination already exists' >&2; exit 1;
10
+ }
11
+ header='^"((@[a-z0-9._-]+/)?[a-z0-9._-]+)@([0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?)":$'
12
+ source='^ - https://registry\.npmjs\.org/[^[:space:]"#]+\.tgz$'
13
+ digest='^ - sha256:([a-f0-9]{64})$'
14
+ invalid() { echo "error: invalid npm pin: $line" >&2; exit 1; }
15
+ state=0
16
+ names=() versions=() archives=() hashes=()
17
+ seen='|'
18
+ while IFS= read -r line || [[ -n "$line" ]]; do
19
+ line=${line%$'\r'}
20
+ [[ "$line" =~ ^[[:space:]]*$ || "$line" == \#* ]] && continue
21
+ case "$state" in
22
+ 0)
23
+ [[ "$line" =~ $header ]] || invalid
24
+ name=${BASH_REMATCH[1]} version=${BASH_REMATCH[3]}
25
+ [[ "$seen" != *"|$name|"* ]] || invalid
26
+ seen="$seen$name|"
27
+ archive=${name#@}; archive=${archive/\//-}-$version.tgz
28
+ state=1 ;;
29
+ 1) [[ "$line" =~ $source ]] || invalid; state=2 ;;
30
+ 2)
31
+ [[ "$line" =~ $digest ]] || invalid
32
+ names+=("$name") versions+=("$version") archives+=("$archive") hashes+=("${BASH_REMATCH[1]}")
33
+ state=0 ;;
34
+ esac
35
+ done < scripts/npm-packages.md
36
+ [[ "$state" == 0 && ${#names[@]} -gt 0 ]] || invalid
37
+ for ((i=0; i<${#names[@]}; i++)); do
38
+ printf '%s %s\n' "${hashes[i]}" "downloads/npm/${archives[i]}" | sha256sum -c -
39
+ done
40
+ mkdir -p "$lib/node_modules/.bin"
41
+ for ((i=0; i<${#names[@]}; i++)); do
42
+ name=${names[i]}
43
+ destination="$lib/node_modules/$name"
44
+ mkdir -p "$destination"
45
+ extra=()
46
+ [[ "$name" != buninu-browser ]] || extra+=(--exclude=package/usr/lib/canvaskit --exclude=package/usr/share/fonts)
47
+ tar -xf "downloads/npm/${archives[i]}" -C "$destination" --strip-components=1 "${extra[@]}"
48
+ # Bin entries of the four SHA-pinned packages; no package scripts execute.
49
+ bin=${name##*/}; entry=index.js
50
+ [[ "$name" != @drxiaozhi/jspulse ]] || entry=jspulse.js
51
+ chmod 755 "$destination/$entry"
52
+ ln -s "../$name/$entry" "$lib/node_modules/.bin/$bin"
53
+ done
54
+ {
55
+ printf '{\n "name": "buninu-system-apps",\n "private": true,\n "dependencies": {\n'
56
+ for ((i=0; i<${#names[@]}; i++)); do
57
+ comma=,
58
+ [[ "$i" != "$((${#names[@]} - 1))" ]] || comma=''
59
+ printf ' "%s": "%s"%s\n' "${names[i]}" "${versions[i]}" "$comma"
60
+ done
61
+ printf ' }\n}\n'
62
+ } > "$lib/package.json"