Author SHA1 Message Date
sneak 62f1f5900d make build-bin: binary uses the system loader on Linux (closes #240)
check / check (push) Successful in 3m11s
nix's bun loads through a glibc in the nix store, and bun build --compile
copied that loader into ~/bin/quak, so nix-collect-garbage could break the
cron backup. On Linux x86-64 the build now passes
--target=bun-linux-x64-baseline, which makes bun use its downloaded release
build with the system loader. Other Linux architectures are refused, since
the fix is untested there; macOS is unchanged.

Model: opus-5-5
2026-11-11 20:33:28 +00:00
6 changed files with 33 additions and 46 deletions

No files matched your search

+13 -1
View File
@@ -28,8 +28,20 @@ build:
# Bundles the built dist/, so the binary reports the version script/build # Bundles the built dist/, so the binary reports the version script/build
# stamped. # stamped.
#
# On Linux, nix's bun loads through a C library in the nix store, and a binary
# bun compiles from itself keeps that loader. nix-collect-garbage can delete
# that C library, and the binary then no longer starts.
# A --target other than the host's makes bun use its downloaded release build,
# which loads through the system's C library. That is tested only on x86-64,
# with the baseline build, so other Linux machines are refused.
build-bin: build build-bin: build
nix-shell -p bun --run "bun build dist/bin/quak.js --compile --outfile bin/quak" case "$$(uname -s)-$$(uname -m)" in \
Linux-x86_64) target=--target=bun-linux-x64-baseline ;; \
Linux-*) echo "make build-bin: on Linux only x86-64 is supported" >&2; exit 1 ;; \
*) target= ;; \
esac; \
nix-shell -p bun --run "bun build dist/bin/quak.js --compile $$target --outfile bin/quak"
install: build-bin install: build-bin
mkdir -p ~/bin mkdir -p ~/bin
+13 -28
View File
@@ -31,8 +31,7 @@ again on the next run. Every download is decrypted on its way to disk.
## Getting Started ## Getting Started
The commands below need `git`, `yarn` and Node 22.13 or later. Node 23 needs The commands below need `git`, Node 22 or later and `yarn`.
23.5 or later.
```bash ```bash
git clone https://git.eeqj.de/sneak/quak.git git clone https://git.eeqj.de/sneak/quak.git
@@ -70,29 +69,11 @@ The library keeps a local copy of the account's album and file lists, the cache.
It brings the cache up to date with the server in rounds called refreshes. It brings the cache up to date with the server in rounds called refreshes.
quak is not published to a package registry: `package.json` marks it `private`, quak is not published to a package registry: `package.json` marks it `private`,
and `dist/` is not committed. Clone and build it as above, then link the clone and `dist/` is not committed. Clone and build it as above, then add the clone to
into your own program: your own program's `package.json` by its path, for example
`yarn add file:../quak`. The package's `main`, `types` and `exports` fields
```bash point into `dist/`, so the clone must be built first. The package is an ES
# In the quak clone: module only, so a CommonJS program cannot `require` it.
yarn link
# In your program's directory:
yarn link quak
```
`yarn link` also installs the `quak` command in yarn's global bin directory, and
`yarn unlink` in the clone removes the link and the command.
A linked quak uses the dependencies that `yarn install` put in the clone, at the
versions in quak's `yarn.lock`. Avoid `yarn add file:../quak`: it resolves
quak's dependencies again, and their newer releases can need a newer Node.
`yarn link quak` does not add quak to your `package.json`, so run it again in
each new checkout of your program.
The package's `main`, `types` and `exports` fields point into `dist/`, so the
clone must be built first. The package is an ES module only, so a CommonJS
program cannot `require` it.
```ts ```ts
import { Client, Library } from "quak"; import { Client, Library } from "quak";
@@ -157,7 +138,10 @@ requests. The "Library reference" below covers both.
one binary and copies it to `~/bin/quak`. Besides what Getting Started needs, one binary and copies it to `~/bin/quak`. Besides what Getting Started needs,
`make install` needs `make` and [nix](https://nixos.org/download). It uses bun, `make install` needs `make` and [nix](https://nixos.org/download). It uses bun,
a JavaScript runtime, to bundle the built `dist/` into one executable. It runs a JavaScript runtime, to bundle the built `dist/` into one executable. It runs
bun through `nix-shell -p bun`. bun through `nix-shell -p bun`. On Linux it works on x86-64 only. There, the
binary is built from bun's own release build rather than nix's bun, so it does
not need the nix store. The first build downloads that release build into
`~/.bun`.
Log in once with `~/bin/quak`, as the user the cron job will run as: Log in once with `~/bin/quak`, as the user the cron job will run as:
@@ -321,7 +305,7 @@ title, with unsafe characters replaced (see "File names from the server"). With
original is kept in the cache as well. original is kept in the cache as well.
A file ID that no collection holds gives `File <id> not found`, and the exit A file ID that no collection holds gives `File <id> not found`, and the exit
status is 1. status is 1. The option `--collection <id>` is accepted and ignored.
A live photo is a still image and a short video that Ente stores as one file. A live photo is a still image and a short video that Ente stores as one file.
`get` writes both, each named after the title with its own extension, as Ente's `get` writes both, each named after the title with its own extension, as Ente's
@@ -1929,7 +1913,8 @@ These apply to people and to coding agents alike.
`bin/clip-keyword-vectors` installs, by SHA-256 hash; `bin/clip-keyword-vectors` installs, by SHA-256 hash;
- Node and yarn, when `script/bootstrap` installs them, by version. - Node and yarn, when `script/bootstrap` installs them, by version.
- Not pinned: - Not pinned:
- bun, which `make build-bin` takes from `nix-shell -p bun`; - bun, which `make build-bin` takes from `nix-shell -p bun`, and on Linux
the release build of the same bun version that it downloads;
- `make` and `git`, which `script/bootstrap` installs from the system - `make` and `git`, which `script/bootstrap` installs from the system
package manager when they are missing, as every `Dockerfile` stage does; package manager when they are missing, as every `Dockerfile` stage does;
- `bash` and `curl`, which `script/bootstrap` installs the same way when it - `bash` and `curl`, which `script/bootstrap` installs the same way when it
+2
View File
@@ -123,6 +123,7 @@ program
.description("Download and decrypt a single file") .description("Download and decrypt a single file")
.argument("<fileID>", "File ID (from `quak files`)") .argument("<fileID>", "File ID (from `quak files`)")
.option("--out <path>", "Output file path") .option("--out <path>", "Output file path")
.option("--collection <id>", "Accepted for compatibility; ignored")
.action((fileID: string, opts: { out?: string }) => .action((fileID: string, opts: { out?: string }) =>
run(getCommand(context(), fileID, opts)), run(getCommand(context(), fileID, opts)),
); );
@@ -132,6 +133,7 @@ program
.description("Download and decrypt a thumbnail") .description("Download and decrypt a thumbnail")
.argument("<fileID>", "File ID (from `quak files`)") .argument("<fileID>", "File ID (from `quak files`)")
.option("--out <path>", "Output file path") .option("--out <path>", "Output file path")
.option("--collection <id>", "Accepted for compatibility; ignored")
.action((fileID: string, opts: { out?: string }) => .action((fileID: string, opts: { out?: string }) =>
run(getThumbCommand(context(), fileID, opts)), run(getThumbCommand(context(), fileID, opts)),
); );
+1 -1
View File
@@ -12,7 +12,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.13.0 || >=23.5.0" "node": ">=22"
}, },
"main": "./dist/src/index.js", "main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts", "types": "./dist/src/index.d.ts",
+2 -14
View File
@@ -1,9 +1,8 @@
/** /**
* Tests for the retry options `bin/quak.ts` loads the saved session with * Tests for the retry options `bin/quak.ts` loads the saved session with
* (`quak backup` gets the unattended ones, every other command the default), * (`quak backup` gets the unattended ones, every other command the default),
* each with the callbacks that log its requests and retries, for `quak files` * each with the callbacks that log its requests and retries, and for
* taking its collection ID as an optional argument, and for `quak get` and * `quak files` taking its collection ID as an optional argument.
* `quak get-thumb` refusing `--collection`.
* *
* Each test runs `bin/quak.ts`, as the smoke test does, with its session loader * Each test runs `bin/quak.ts`, as the smoke test does, with its session loader
* replaced by one that records the options it is given and reports no session, * replaced by one that records the options it is given and reports no session,
@@ -106,14 +105,3 @@ describe("bin/quak.ts files", () => {
expect(await quak("files", "7")).toMatch(/^Not logged in/); expect(await quak("files", "7")).toMatch(/^Not logged in/);
}); });
}); });
describe("bin/quak.ts get and get-thumb", () => {
it.each(["get", "get-thumb"])(
"%s refuses --collection as an unknown option",
async (command) => {
expect(await quak(command, "1", "--collection", "2")).toMatch(
/^error: unknown option '--collection'/,
);
},
);
});
+2 -2
View File
@@ -569,14 +569,14 @@ describe("files", () => {
}); });
describe("get and get-thumb", () => { describe("get and get-thumb", () => {
it("get finds a file in any album", async () => { it("get finds a file in any album without --collection", async () => {
const out = join(root, "diagram.png"); const out = join(root, "diagram.png");
expect(await getCommand(context(), "200", { out })).toBe(0); expect(await getCommand(context(), "200", { out })).toBe(0);
expect(readFileSync(out)).toEqual(Buffer.alloc(7, 200)); expect(readFileSync(out)).toEqual(Buffer.alloc(7, 200));
expect(stderr.text).toBe(`7 bytes -> ${out}\n`); expect(stderr.text).toBe(`7 bytes -> ${out}\n`);
}); });
it("get-thumb finds a file in any album", async () => { it("get-thumb finds a file in any album without --collection", async () => {
const out = join(root, "thumb.jpg"); const out = join(root, "thumb.jpg");
expect(await getThumbCommand(context(), "200", { out })).toBe(0); expect(await getThumbCommand(context(), "200", { out })).toBe(0);
expect(readFileSync(out)).toEqual(Buffer.alloc(3, 200)); expect(readFileSync(out)).toEqual(Buffer.alloc(3, 200));