Files
mitch030504--AstroQuest-Frame/tools/mk-sysroot.mjs
T
bigmak94andClaude Opus 5.5 b5f400bbbd AstroQuest 0.11: ASTRO BOT Rescue Mission in VR on Meta Quest 3 and PC VR
On top of the shadps4-arm64 core imported in the previous commit:

- PlayStation VR emulation: headset, tracker, reprojection, camera and social screen; the
  player's seat and view reset; head velocity for head-butts; field of view settings
- Sound: a native Ngs2 engine (samplers, submixers, effects), the Audio3d spatializer, virtual
  surround for headset speakers, microphone pass-through; libSceJson2; libSceFiber on FEX
- Fixes for CUSA12392 under emulation: real-time step, pace and resolution governor, larger
  render sizes on the PC, save loading at start
- Renderer: geometry shaders fed with fans/polygons/loops, cube-array layers of light probes,
  sample-count mismatches on Adreno, timeline semaphore hang, FXAA in place of the MSAA
  resolve, sharpening, VR frame export
- quest-host: the Quest app (OpenXR host that runs the glibc core as a child process, frames
  through AHardwareBuffers, DualSense mapping, hand-tracked controller placement, KGSL shim)
- PC: in-process OpenXR host (Virtual Desktop or any OpenXR runtime), Touch controllers as the
  gamepad, launcher with a settings window; vendored OpenXR SDK 1.1.63
- Builds keep the build machine's folders out of the binaries (-ffile-prefix-map); machine
  settings (Python, headset serial, signing key) live in git-ignored tools/*.local files
- tools: build, packaging and test scripts for Windows and the headset
- README with installation, license (GPL-2.0-or-later), third-party notices and thanks

No game files are part of the repository; .gitignore only lets listed paths in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:00:43 +02:00

202 lines
7.8 KiB
JavaScript

// Assembles a Debian arm64 sysroot on a machine without WSL, a package manager or symlink
// privileges, for cross-compiling the emulator core with clang.
// node mk-sysroot.mjs <sysroot dir> [--suite trixie] [--cache dir] <package>...
// Packages are taken as listed (no dependency resolution). Symbolic links inside the packages
// become copies; /lib and friends become junctions onto /usr.
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
copyFileSync, existsSync, mkdirSync, readFileSync, statSync, symlinkSync, writeFileSync,
} from "node:fs";
import { dirname, join, posix, resolve } from "node:path";
const args = process.argv.slice(2);
const sysroot = resolve(args.shift() ?? "");
let suite = "trixie";
let cache = join(dirname(sysroot), "deb-cache");
const wanted = [];
while (args.length) {
const arg = args.shift();
if (arg === "--suite") suite = args.shift();
else if (arg === "--cache") cache = args.shift();
else wanted.push(arg);
}
if (!sysroot || !wanted.length) {
console.error("usage: mk-sysroot.mjs <sysroot dir> [--suite trixie] [--cache dir] <package>...");
process.exit(1);
}
mkdirSync(cache, { recursive: true });
mkdirSync(sysroot, { recursive: true });
const mirror = "https://deb.debian.org/debian";
function decompress(tool, data) {
const result = spawnSync(tool, ["-dc"], { input: data, maxBuffer: 1 << 30 });
if (result.status !== 0) throw new Error(`${tool} failed: ${result.stderr}`);
return result.stdout;
}
async function download(url, file) {
if (existsSync(file) && statSync(file).size > 0) return readFileSync(file);
const response = await fetch(url);
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
const data = Buffer.from(await response.arrayBuffer());
writeFileSync(file, data);
return data;
}
// Later indexes win, so point-release and security updates replace the base version.
const indexes = [
[`${mirror}/dists/${suite}/main/binary-arm64/Packages.xz`, mirror],
[`${mirror}/dists/${suite}-updates/main/binary-arm64/Packages.xz`, mirror],
];
const packages = new Map();
for (const [url, base] of indexes) {
const file = join(cache, createHash("sha1").update(url).digest("hex").slice(0, 12) + "-Packages.xz");
let text;
try {
text = decompress("xz", await download(url, file)).toString("utf8");
} catch (error) {
console.warn(`skipping ${url}: ${error.message}`);
continue;
}
for (const stanza of text.split("\n\n")) {
const name = /^Package: (.+)$/m.exec(stanza)?.[1];
const filename = /^Filename: (.+)$/m.exec(stanza)?.[1];
const sha256 = /^SHA256: (.+)$/m.exec(stanza)?.[1];
const version = /^Version: (.+)$/m.exec(stanza)?.[1];
if (name && filename) packages.set(name, { url: `${base}/${filename}`, sha256, version });
}
}
function* arMembers(deb) {
if (deb.toString("latin1", 0, 8) !== "!<arch>\n") throw new Error("not an ar archive");
let pos = 8;
while (pos + 60 <= deb.length) {
const name = deb.toString("latin1", pos, pos + 16).trim().replace(/\/$/, "");
const size = Number(deb.toString("latin1", pos + 48, pos + 58).trim());
yield { name, data: deb.subarray(pos + 60, pos + 60 + size) };
pos += 60 + size + (size & 1);
}
}
function* tarEntries(tar) {
let pos = 0;
let longName = null, longLink = null;
let pax = {};
const text = (start, length) => {
const slice = tar.subarray(start, start + length);
const end = slice.indexOf(0);
return slice.toString("utf8", 0, end < 0 ? length : end);
};
while (pos + 512 <= tar.length) {
if (tar[pos] === 0) break;
let name = text(pos, 100);
const size = parseInt(text(pos + 124, 12).trim() || "0", 8);
const type = String.fromCharCode(tar[pos + 156] || 0x30);
let link = text(pos + 157, 100);
const prefix = text(pos + 345, 155);
if (prefix) name = `${prefix}/${name}`;
const data = tar.subarray(pos + 512, pos + 512 + size);
pos += 512 + Math.ceil(size / 512) * 512;
if (type === "L") { longName = data.toString("utf8").replace(/\0+$/, ""); continue; }
if (type === "K") { longLink = data.toString("utf8").replace(/\0+$/, ""); continue; }
if (type === "x" || type === "g") {
for (const record of data.toString("utf8").split("\n")) {
const match = /^\d+ ([^=]+)=(.*)$/.exec(record);
if (match) pax[match[1]] = match[2];
}
continue;
}
name = pax.path ?? longName ?? name;
link = pax.linkpath ?? longLink ?? link;
longName = longLink = null;
pax = {};
yield { name: name.replace(/^\.\//, "").replace(/\/$/, ""), type, link, data };
}
}
// Merged /usr: everything top-level that Debian symlinks into /usr is stored there directly.
const merged = ["lib", "bin", "sbin", "lib64"];
const canonical = (path) => {
const parts = path.split("/").filter(Boolean);
if (merged.includes(parts[0])) parts.unshift("usr");
return parts.join("/");
};
const links = [];
let fileCount = 0;
for (const name of wanted) {
const pkg = packages.get(name);
if (!pkg) throw new Error(`package ${name} not found in ${suite}`);
const debFile = join(cache, posix.basename(pkg.url));
const deb = await download(pkg.url, debFile);
if (pkg.sha256 && createHash("sha256").update(deb).digest("hex") !== pkg.sha256) {
throw new Error(`${name}: checksum mismatch`);
}
let tar = null;
for (const member of arMembers(deb)) {
if (member.name === "data.tar.xz") tar = decompress("xz", member.data);
else if (member.name === "data.tar.zst") tar = decompress("zstd", member.data);
else if (member.name === "data.tar.gz") tar = decompress("gzip", member.data);
else if (member.name === "data.tar") tar = member.data;
}
if (!tar) throw new Error(`${name}: no data archive`);
for (const entry of tarEntries(tar)) {
if (!entry.name) continue;
const path = canonical(entry.name);
const target = join(sysroot, path);
if (entry.type === "5") {
mkdirSync(target, { recursive: true });
} else if (entry.type === "0" || entry.type === "7") {
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, entry.data);
fileCount++;
} else if (entry.type === "2" || entry.type === "1") {
// Hard links name their target from the archive root, symlinks from their own directory.
const resolved = entry.type === "1" || entry.link.startsWith("/")
? canonical(entry.link)
: canonical(posix.normalize(posix.join(posix.dirname(entry.name), entry.link)));
if (!path.startsWith("usr/share/doc/")) links.push({ path, resolved });
}
}
console.log(`${name} ${pkg.version}`);
}
// "include" is not a Debian link: clang finds GCC through /lib and then walks up with "..",
// which Windows resolves lexically instead of through the junction.
for (const dir of [...merged, "include"]) {
const junction = join(sysroot, dir);
if (!existsSync(junction) && existsSync(join(sysroot, "usr", dir))) {
symlinkSync(join(sysroot, "usr", dir), junction, "junction");
}
}
// Links may point at other links, so keep resolving until nothing changes.
let pending = links;
let linked = 0;
for (let pass = 0; pass < 8 && pending.length; pass++) {
const next = [];
for (const link of pending) {
const source = join(sysroot, link.resolved);
const target = join(sysroot, link.path);
if (!existsSync(source)) {
next.push(link);
continue;
}
if (statSync(source).isDirectory()) {
if (!existsSync(target)) {
mkdirSync(dirname(target), { recursive: true });
symlinkSync(source, target, "junction");
}
} else {
mkdirSync(dirname(target), { recursive: true });
copyFileSync(source, target);
}
linked++;
}
pending = next;
}
console.log(`${fileCount} files, ${linked} links materialised, ${pending.length} dangling links skipped`);
for (const link of pending.slice(0, 40)) console.log(` dangling: ${link.path} -> ${link.resolved}`);