// `script/build-bin` compiles `bin/quak`, the single binary that the README's // cron setup runs. bun compiles it from a copy of its own executable, so the // binary loads through the same C library as the bun that built it. nix's bun // loads through a C library in the nix store, which garbage collection can // delete. On Linux the script therefore downloads bun's own release build, // which loads through the system's C library. // // What the script promises, and why each test matters: // // - On Linux, each machine and C library that bun publishes a build for gets // its own archive: x86-64 or arm64, with glibc or with musl, as on Alpine. A // bun built for another machine or C library does not start, and // `make install` fails. // - The archive's SHA-256 must match the one in the script before anything in // the archive runs. Without that check, a tampered download would build the // binary that cron runs. // - Linux on any other machine stops with an error. Building there with nix's // bun would bring back the dependency on the nix store. // - Other systems, such as macOS, compile with nix's bun as before, and // download nothing. // // Each test copies the script into a fresh directory, which the script then // treats as the checkout, and runs it there. Stubs first on PATH stand in for // `uname`, `ldd`, `curl`, `unzip` and `nix-shell`, so no test downloads // anything or runs a real bun. import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); // The stubs, keyed by the command each replaces. `uname` and `ldd` describe the // system that runBuildBin passes in the environment. `curl`, `unzip` and // `nix-shell` write the arguments they were given, one per line, to a file // named after the command in $STUB_LOG_DIR, which also shows that they ran. const stubs: Record = { uname: ` case "$1" in -s) echo "$STUB_UNAME_S" ;; -m) echo "$STUB_UNAME_M" ;; esac`, // The script looks only at the start of the line: "musl libc" or not. ldd: ` if [ "$STUB_LIBC" = musl ]; then echo "musl libc (x86_64)" else echo "ldd (GNU libc) 2.36" fi`, // Writes a file that is not the archive to the path after -o. curl: ` printf '%s\\n' "$@" > "$STUB_LOG_DIR/curl" while [ $# -gt 1 ]; do if [ "$1" = -o ]; then echo "not the archive" > "$2"; fi shift done`, // Unpacks a stub bun where the real archive puts bun: in a directory named // after the archive, inside the directory after -d. That bun writes // bin/quak, as a real compile would. unzip: ` printf '%s\\n' "$@" > "$STUB_LOG_DIR/unzip" for arg in "$@"; do case "$arg" in *.zip) archive="$arg" ;; esac if [ "$previous" = -d ]; then directory="$arg"; fi previous="$arg" done bun="$directory/$(basename "$archive" .zip)/bun" mkdir -p "$(dirname "$bun")" echo '#!/bin/sh' > "$bun" echo 'mkdir -p bin && echo binary > bin/quak' >> "$bun" chmod +x "$bun"`, "nix-shell": ` printf '%s\\n' "$@" > "$STUB_LOG_DIR/nix-shell"`, }; let checkout = ""; const writeScript = (path: string, body: string): void => { writeFileSync(path, `#!/bin/sh\n${body}\n`); chmodSync(path, 0o755); }; // A checkout holding the script, a stub script/build that does nothing, the // stubs, and the directory they log to. beforeEach(() => { checkout = mkdtempSync(join(tmpdir(), "quak-build-bin-")); mkdirSync(join(checkout, "script")); copyFileSync( join(repoRoot, "script/build-bin"), join(checkout, "script/build-bin"), ); chmodSync(join(checkout, "script/build-bin"), 0o755); writeScript(join(checkout, "script/build"), "exit 0"); mkdirSync(join(checkout, "stubs")); for (const [command, body] of Object.entries(stubs)) { writeScript(join(checkout, "stubs", command), body); } mkdirSync(join(checkout, "log")); }); afterEach(() => { rmSync(checkout, { recursive: true, force: true }); }); // Runs the script with nothing in its environment but the stubs' settings and a // PATH that finds the stubs first. const runBuildBin = (system: string, machine: string, libc = "glibc") => spawnSync(join(checkout, "script/build-bin"), { cwd: checkout, encoding: "utf-8", env: { PATH: `${join(checkout, "stubs")}:${process.env.PATH}`, STUB_UNAME_S: system, STUB_UNAME_M: machine, STUB_LIBC: libc, STUB_LOG_DIR: join(checkout, "log"), }, }); const ran = (command: string): boolean => existsSync(join(checkout, "log", command)); const argumentsOf = (command: string): string[] => readFileSync(join(checkout, "log", command), "utf-8") .trimEnd() .split("\n"); describe("script/build-bin", () => { // The stub curl writes a file that is not the archive, so the hash check // must stop the script before unzip runs. A script without the check would // run unzip, and the stub bun it unpacks would write bin/quak. it.each([ ["x86_64", "glibc", "bun-linux-x64-baseline.zip"], ["x86_64", "musl", "bun-linux-x64-musl-baseline.zip"], ["aarch64", "glibc", "bun-linux-aarch64.zip"], ["aarch64", "musl", "bun-linux-aarch64-musl.zip"], ])( "on Linux %s with %s, downloads %s and stops when its hash does not match", (machine, libc, archive) => { const result = runBuildBin("Linux", machine, libc); expect(argumentsOf("curl")).toContain( "https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/" + archive, ); expect(result.status).not.toBe(0); expect(ran("unzip")).toBe(false); expect(existsSync(join(checkout, "bin/quak"))).toBe(false); }, ); it("stops on a Linux machine that bun publishes no build for", () => { const result = runBuildBin("Linux", "riscv64"); expect(result.status).not.toBe(0); expect(ran("curl")).toBe(false); expect(ran("nix-shell")).toBe(false); }); it("compiles with nix's bun on macOS", () => { const result = runBuildBin("Darwin", "arm64"); expect(result.status).toBe(0); expect(ran("curl")).toBe(false); expect(argumentsOf("nix-shell")).toEqual([ "-p", "bun", "--run", "bun build dist/bin/quak.js --compile --outfile bin/quak", ]); }); });