Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,4 @@ jobs:
run: bun test --timeout 60000 --preload ./src/test-preload.ts test/
env:
CI: true
DYLD_LIBRARY_PATH: /opt/homebrew/opt/sqlite/lib
LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu
24 changes: 8 additions & 16 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
};
};
} //
flake-utils.lib.eachDefaultSystem (system:
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system:
let
pkgs = nixpkgs.legacyPackages.${system};
packageJson = builtins.fromJSON (builtins.readFile ./package.json);
Expand All @@ -45,11 +45,9 @@

nodeModulesHashes = {
x86_64-linux = "sha256-kWfm4L689mdxDtH976p/eLuQMO5NkPKXh+vhT2dqkcs=";
aarch64-darwin = "sha256-9vvR3KLmBc+4bfyWEyyM8FHWg+DfiDzUlwqUlm3NFc8=";

# Populate these on first build for additional hosts if/when needed.
# Populate on first build if/when needed.
aarch64-linux = pkgs.lib.fakeHash;
x86_64-darwin = pkgs.lib.fakeHash;
};

nodeModules = pkgs.stdenvNoCC.mkDerivation {
Expand Down Expand Up @@ -104,8 +102,6 @@
pkgs.nodejs
pkgs.node-gyp
pkgs.python3 # needed by node-gyp to compile better-sqlite3
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isDarwin [
pkgs.darwin.cctools # provides libtool needed by node-gyp on macOS
];

buildInputs = [ pkgs.sqlite ];
Expand All @@ -130,24 +126,20 @@

# The flake wraps `bun src/cli/qmd.ts` directly, so bin/qmd never
# runs. Mirror its pre-import env here (#723): quiet llama/ggml
# native logs for `qmd mcp` (stdio is JSON-RPC), and disable Metal
# residency sets on Darwin so ggml's process-static destructor
# does not dump a stack trace after a successful query
# (ggml-org/llama.cpp#22593). `--run` fires before bun starts, so
# the env is in place before the native binding loads. Preserve
# explicit user values; QMD_METAL_KEEP_RESIDENCY=1 opts back in.
# native logs for `qmd mcp` (stdio is JSON-RPC). `--run` fires
# before bun starts, so the env is in place before the native
# binding loads. Preserve explicit user values.
makeWrapper ${pkgs.bun}/bin/bun $out/bin/qmd \
--add-flags "$out/lib/qmd/src/cli/qmd.ts" \
--set DYLD_LIBRARY_PATH "${pkgs.sqlite.out}/lib" \
--set LD_LIBRARY_PATH "${pkgs.sqlite.out}/lib${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux ":${pkgs.stdenv.cc.libc.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib"}" \
--run 'if [ "$1" = mcp ]; then export LLAMA_LOG_LEVEL="''${LLAMA_LOG_LEVEL:-error}"; export GGML_LOG_LEVEL="''${GGML_LOG_LEVEL:-error}"; export GGML_BACKEND_SILENT="''${GGML_BACKEND_SILENT:-1}"; fi; if [ "$(uname -s)" = Darwin ] && [ "''${QMD_METAL_KEEP_RESIDENCY:-}" != 1 ]; then export GGML_METAL_NO_RESIDENCY="''${GGML_METAL_NO_RESIDENCY:-1}"; fi'
--set LD_LIBRARY_PATH "${pkgs.sqlite.out}/lib:${pkgs.stdenv.cc.libc.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib" \
--run 'if [ "$1" = mcp ]; then export LLAMA_LOG_LEVEL="''${LLAMA_LOG_LEVEL:-error}"; export GGML_LOG_LEVEL="''${GGML_LOG_LEVEL:-error}"; export GGML_BACKEND_SILENT="''${GGML_BACKEND_SILENT:-1}"; fi'
'';

meta = with pkgs.lib; {
description = "On-device search engine for markdown notes, meeting transcripts, and knowledge bases";
homepage = "https://github.com/tobi/qmd";
license = licenses.mit;
platforms = platforms.unix;
platforms = platforms.linux;
mainProgram = "qmd";
};
};
Expand Down
13 changes: 1 addition & 12 deletions scripts/test-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,14 @@ import { fileURLToPath } from "node:url";

const root = fileURLToPath(new URL("..", import.meta.url));

// Mirror bin/qmd's darwin Metal residency mitigation for test subprocesses.
// libggml-metal asserts on a non-empty residency set during its static
// destructor (ggml-org/llama.cpp#22593, fix open as #22595) and dumps a
// multi-kB backtrace at process exit even when tests pass. The env var must
// be set BEFORE the subprocess starts because libggml-metal reads it via
// libc getenv at module-load time. Opt out with QMD_METAL_KEEP_RESIDENCY=1.
const darwinMetalEnv =
process.platform === "darwin" && process.env.QMD_METAL_KEEP_RESIDENCY !== "1"
? { GGML_METAL_NO_RESIDENCY: "1" }
: {};

function run(label, command, args, options = {}) {
console.log(`==> ${label}`);
const { env: extraEnv, ...spawnOptions } = options;
const result = spawnSync(command, args, {
cwd: root,
stdio: "inherit",
shell: process.platform === "win32",
env: { ...process.env, ...darwinMetalEnv, ...(extraEnv ?? {}) },
env: { ...process.env, ...(extraEnv ?? {}) },
...spawnOptions,
});
if (result.status !== 0) {
Expand Down
6 changes: 1 addition & 5 deletions src/test-preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
* BEFORE bun/node starts (libggml-metal reads it via libc getenv at
* module load). Bun does not propagate `process.env` writes to libc
* setenv, so setting it from here would be a no-op for the native
* binding. The env var is injected by:
* - bin/qmd for production CLI runs
* - scripts/test-all.mjs for `npm test`
* - package.json test:bun / test:unit scripts for direct invocation
* See CLAUDE.md for invoking `bun test` manually on darwin.
* binding. bin/qmd injects the env var for production CLI runs.
*/
import { afterAll } from "bun:test";
import { disposeDefaultLlamaCpp } from "./llm";
Expand Down
40 changes: 22 additions & 18 deletions test/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,29 +82,33 @@ describe("Nix flake package layout", () => {
expect(flake).toContain("cp package.json $out/lib/qmd/");
});

test("makeWrapper seeds the same pre-import env as bin/qmd (#723)", () => {
test("makeWrapper seeds the Linux pre-import env (#723)", () => {
const flake = readFileSync(new URL("flake.nix", root), "utf8");
const launcher = readFileSync(new URL("bin/qmd", root), "utf8");
const wrapper = flake.match(/makeWrapper \$\{pkgs\.bun\}\/bin\/bun[\s\S]*?\n\s*'';/)?.[0];
expect(wrapper, "flake.nix should wrap bun with makeWrapper").toBeDefined();

// Nix installs skip bin/qmd and exec bun src/cli/qmd.ts. The wrapper must
// still set these BEFORE the native binding loads, matching the launcher.
for (const env of [
"LLAMA_LOG_LEVEL",
"GGML_LOG_LEVEL",
"GGML_BACKEND_SILENT",
"GGML_METAL_NO_RESIDENCY",
"QMD_METAL_KEEP_RESIDENCY",
]) {
// still quiet native logs for `qmd mcp` BEFORE the binding loads,
// matching the launcher.
for (const env of ["LLAMA_LOG_LEVEL", "GGML_LOG_LEVEL", "GGML_BACKEND_SILENT"]) {
expect(launcher, `bin/qmd should set ${env}`).toContain(env);
expect(flake, `flake.nix wrapper should set ${env}`).toContain(env);
}

expect(flake).toContain('--run');
expect(flake).toContain('$1" = mcp');
expect(flake).toContain('$(uname -s)" = Darwin');
expect(flake).toContain('LLAMA_LOG_LEVEL:-error');
expect(flake).toContain('GGML_LOG_LEVEL:-error');
expect(flake).toContain('GGML_BACKEND_SILENT:-1');
expect(flake).toContain('GGML_METAL_NO_RESIDENCY:-1');
expect(wrapper).toContain(`--run 'if [ "$1" = mcp ]; then`);
expect(wrapper).toContain('export LLAMA_LOG_LEVEL="\'\'${LLAMA_LOG_LEVEL:-error}"');
expect(wrapper).toContain('export GGML_LOG_LEVEL="\'\'${GGML_LOG_LEVEL:-error}"');
expect(wrapper).toContain('export GGML_BACKEND_SILENT="\'\'${GGML_BACKEND_SILENT:-1}"');

// sqlite-vec and the native modules resolve sqlite, libc and libstdc++
// from the store, not the host.
expect(wrapper).toContain(
'--set LD_LIBRARY_PATH "${pkgs.sqlite.out}/lib:${pkgs.stdenv.cc.libc.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib"',
);

// The flake builds Linux only; macOS loader and Metal env have no place in it.
for (const env of ["GGML_METAL_NO_RESIDENCY", "QMD_METAL_KEEP_RESIDENCY", "DYLD_LIBRARY_PATH"]) {
expect(flake, `flake.nix should not set ${env}`).not.toContain(env);
}
expect(flake).not.toContain("uname -s");
});
});
Loading