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.
- package/Dockerfile +5 -2
- package/NOTICE.md +110 -49
- package/README.md +439 -144
- package/fetch-npm.sh +44 -0
- package/fetch-plus-build.sh +1 -0
- package/index.js +15 -13
- package/initramfs/bin/buninu-browser +24 -0
- package/initramfs/bin/jspinyin +12 -0
- package/initramfs/bin/jspulse +2 -0
- package/initramfs/bin/tar +40 -6
- package/initramfs/bin/top +1 -7
- package/initramfs/buninu/CHANGELOG.md +4 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +28 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/main.js +7 -5
- package/initramfs/buninu/apps/bunmsh/src/shell.js +584 -75
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +64 -0
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +5 -0
- package/initramfs/lib/browser-resources.js +25 -0
- package/initramfs/lib/tar-exclude.js +20 -0
- package/initramfs/lib/tar-strip.js +94 -0
- package/initramfs/lib/top-memory.js +12 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +24 -1
- package/initramfs/usr/share/doc/buninu-linux/top.md +18 -0
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/fetch-npm.js +51 -0
- package/scripts/fetch.js +1 -0
- package/scripts/js-tools.js +3 -0
- package/scripts/npm-packages.md +18 -0
- package/scripts/pack-initramfs.sh +1 -0
- package/scripts/pack.js +2 -0
- package/scripts/stage-npm.js +57 -0
- package/scripts/stage-npm.sh +62 -0
- package/test/fetch-npm.test.js +92 -0
- package/test/host/Dockerfile +6 -4
- package/test/host/boot-test.exp +2 -2
- package/test/host/host-test.sh +2 -2
- package/test/host/in-container.sh +4 -4
- package/test/js-tools-selection.test.js +19 -0
- package/test/npm-launchers.test.js +59 -0
- package/test/stage-npm.test.js +130 -0
- package/test/tar-strip.test.js +218 -0
- package/test/top-memory.test.js +23 -0
- 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 () => {
|
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` |
|
|
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
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: "
|
|
20
|
-
linuxLtsSha256: "
|
|
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: "
|
|
36
|
-
linuxLtsSha256: "
|
|
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.
|
|
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.
|
|
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=
|
|
67
|
-
linux_lts_sha256=
|
|
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=
|
|
86
|
-
linux_lts_sha256=
|
|
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,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"
|