Test script/build-bin's archive choice, hash check and refusal
check / check (push) Successful in 3m51s
check / check (push) Successful in 3m51s
The test runs the script with stub uname, ldd, curl, unzip and nix-shell first on PATH, so it needs no network and no bun. The download and the compile stay untested. Model: opus-5-5
This commit is contained in:
1 parent
674fdeb482
commit
685f1d584c
1 file changed
+180
@@ -0,0 +1,180 @@
|
||||
// `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<string, string> = {
|
||||
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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user