// `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 the same libraries as the bun that built it. nix's bun loads // libraries from the nix store, which garbage collection can delete: on Linux // its C library, on macOS its ICU. The script therefore downloads bun's own // release build, which loads only the system's libraries. // // What the script promises, and why each test matters: // // - Each system, machine and C library that bun publishes a build for gets its // own archive: Linux on x86-64 or arm64, with glibc or with musl, as on // Alpine, and macOS on Intel or Apple silicon. A bun built for another // system, 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. // - Any other system or machine stops with an error, and never falls back to // nix's bun. Building with nix's bun would bring back the dependency on the // nix store. // // 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`, `nix-shell` and `shasum`, so no test // downloads anything or runs a real bun. import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, 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`, // `nix-shell` and `shasum` 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"`, // The script checks the hash with `shasum` where there is no `sha256sum`. // The stub fails, as `shasum` does when the hash does not match. shasum: ` printf '%s\\n' "$@" > "$STUB_LOG_DIR/shasum" exit 1`, }; 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. Unless the caller passes one, the PATH finds the stubs first and then // the host's commands. const runBuildBin = ( system: string, machine: string, { libc = "glibc", path = `${join(checkout, "stubs")}:${process.env.PATH}`, } = {}, ) => spawnSync(join(checkout, "script/build-bin"), { cwd: checkout, encoding: "utf-8", env: { PATH: path, STUB_UNAME_S: system, STUB_UNAME_M: machine, STUB_LIBC: libc, STUB_LOG_DIR: join(checkout, "log"), }, }); // The real commands that the script and the stubs run. const hostCommands = [ "basename", "chmod", "dirname", "grep", "mkdir", "mktemp", "rm", ]; // A Mac has no `ldd`, and not every Mac has `sha256sum`. This makes a PATH that // finds neither: the stubs other than `ldd`, then links to hostCommands. const makeMacPath = (): string => { rmSync(join(checkout, "stubs", "ldd")); mkdirSync(join(checkout, "host-commands")); for (const command of hostCommands) { const target = execFileSync("sh", ["-c", `command -v ${command}`], { encoding: "utf-8", }).trim(); symlinkSync(target, join(checkout, "host-commands", command)); } return `${join(checkout, "stubs")}:${join(checkout, "host-commands")}`; }; 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([ ["Linux", "x86_64", "glibc", "bun-linux-x64-baseline.zip"], ["Linux", "x86_64", "musl", "bun-linux-x64-musl-baseline.zip"], ["Linux", "aarch64", "glibc", "bun-linux-aarch64.zip"], ["Linux", "aarch64", "musl", "bun-linux-aarch64-musl.zip"], ])( "on %s %s with %s, downloads %s and stops when its hash does not match", (system, machine, libc, archive) => { const result = runBuildBin(system, 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(ran("nix-shell")).toBe(false); expect(existsSync(join(checkout, "bin/quak"))).toBe(false); }, ); // On the PATH from makeMacPath the script finds no `sha256sum` and checks // the hash with the stub `shasum`. That stub fails, so the script must stop // before unzip runs. // On Apple silicon `uname -m` says arm64, where bun's archive says aarch64. it.each([ ["Darwin", "x86_64", "bun-darwin-x64-baseline.zip"], ["Darwin", "arm64", "bun-darwin-aarch64.zip"], ])( "on %s %s, downloads %s and stops when shasum rejects its hash", (system, machine, archive) => { const result = runBuildBin(system, machine, { path: makeMacPath(), }); expect(argumentsOf("curl")).toContain( "https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/" + archive, ); expect(argumentsOf("shasum")).toEqual(["-a", "256", "-c", "-"]); expect(result.status).not.toBe(0); expect(ran("unzip")).toBe(false); expect(ran("nix-shell")).toBe(false); expect(existsSync(join(checkout, "bin/quak"))).toBe(false); }, ); it.each([ ["Linux", "riscv64"], ["FreeBSD", "amd64"], ])( "stops on %s %s, which bun publishes no build for", (system, machine) => { const result = runBuildBin(system, machine); expect(result.status).not.toBe(0); expect(ran("curl")).toBe(false); expect(ran("nix-shell")).toBe(false); }, ); });