From 50337b3f338f6bb84c4927013189fa01e7eb513a Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 7 Oct 2026 18:34:29 +0000 Subject: [PATCH 1/2] chore: drop darwin from flake (Linux-only) Org rule: no macOS in systemfsoftware repos. - flake.nix: eachDefaultSystem -> eachSystem [ x86_64-linux aarch64-linux ]; drop the aarch64-darwin/x86_64-darwin node-modules hashes, the isDarwin pkgs.darwin.cctools nativeBuildInputs, DYLD_LIBRARY_PATH, and the Darwin Metal residency branch of the wrapper --run script (MCP log-level env unchanged); LD_LIBRARY_PATH is now unconditional (same value as before on Linux); meta.platforms = platforms.linux. - ci.yml: drop the Homebrew DYLD_LIBRARY_PATH env from the bun test step. - scripts/test-all.mjs: drop the darwin-only GGML_METAL_NO_RESIDENCY env; src/test-preload.ts comment updated to match. --- .github/workflows/ci.yml | 1 - flake.nix | 24 ++++++++---------------- scripts/test-all.mjs | 13 +------------ src/test-preload.ts | 6 +----- 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27f0f631e..211c1f779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/flake.nix b/flake.nix index c77745484..5afc62476 100644 --- a/flake.nix +++ b/flake.nix @@ -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); @@ -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 { @@ -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 ]; @@ -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"; }; }; diff --git a/scripts/test-all.mjs b/scripts/test-all.mjs index bd4f660b0..5dafc4d1e 100644 --- a/scripts/test-all.mjs +++ b/scripts/test-all.mjs @@ -5,17 +5,6 @@ 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; @@ -23,7 +12,7 @@ function run(label, command, args, options = {}) { cwd: root, stdio: "inherit", shell: process.platform === "win32", - env: { ...process.env, ...darwinMetalEnv, ...(extraEnv ?? {}) }, + env: { ...process.env, ...(extraEnv ?? {}) }, ...spawnOptions, }); if (result.status !== 0) { diff --git a/src/test-preload.ts b/src/test-preload.ts index f10909ce7..e3bb79b35 100644 --- a/src/test-preload.ts +++ b/src/test-preload.ts @@ -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"; From 5de0972aec75e62d963a5dd00121a6d9d7d45817 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 7 Oct 2026 18:48:54 +0000 Subject: [PATCH 2/2] test: assert the Linux-only flake wrapper env The flake wrapper no longer carries the darwin Metal residency branch, so "makeWrapper seeds the same pre-import env as bin/qmd (#723)" failed on GGML_METAL_NO_RESIDENCY. Retarget it at what the Linux wrapper seeds: the mcp log-level exports (LLAMA_LOG_LEVEL, GGML_LOG_LEVEL, GGML_BACKEND_SILENT) and LD_LIBRARY_PATH, scoped to the makeWrapper invocation, and assert flake.nix sets none of GGML_METAL_NO_RESIDENCY, QMD_METAL_KEEP_RESIDENCY or DYLD_LIBRARY_PATH. --- test/package.test.ts | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/test/package.test.ts b/test/package.test.ts index d70cd4b11..3c8386eda 100644 --- a/test/package.test.ts +++ b/test/package.test.ts @@ -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"); }); });