diff --git a/CHANGELOG.md b/CHANGELOG.md index d530dfaa..68f0bb1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### Fixes +- NixOS: fix `qmd embed` crash on immutable-root systems. Prebuilt + node-llama-cpp binaries expect FHS paths like `/lib64/libc.so.6` + which don't exist on NixOS. The flake wrapper sets `LD_LIBRARY_PATH` + to include Nix's glibc and libstdc++. When the install directory + is read-only, `getLlama()` uses `build: "never"` to skip source + builds that would fail with EACCES. - GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529 - Fix: preserve original filename case in `handelize()`. The previous `.toLowerCase()` call made indexed paths unreachable on case-sensitive diff --git a/flake.lock b/flake.lock index 3fc91a9b..264c8d4b 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769188852, - "narHash": "sha256-aBAGyMum27K7cP5OR7BMioJOF3icquJMZDDgk6ZEg1A=", + "lastModified": 1775888245, + "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a1bab9e494f5f4939442a57a58d0449a109593fe", + "rev": "13043924aaa7375ce482ebe2494338e058282925", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 36450130..60431757 100644 --- a/flake.nix +++ b/flake.nix @@ -6,9 +6,20 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils }: + outputs = { - homeModules.default = { config, lib, pkgs, ... }: + self, + nixpkgs, + flake-utils, + }: + { + homeModules.default = + { + config, + lib, + pkgs, + ... + }: with lib; let cfg = config.programs.qmd; @@ -29,8 +40,9 @@ home.packages = [ cfg.package ]; }; }; - } // - flake-utils.lib.eachDefaultSystem (system: + } + // flake-utils.lib.eachDefaultSystem ( + system: let pkgs = nixpkgs.legacyPackages.${system}; packageJson = builtins.fromJSON (builtins.readFile ./package.json); @@ -38,7 +50,7 @@ # SQLite with loadable extension support for sqlite-vec sqliteWithExtensions = pkgs.sqlite.overrideAttrs (old: { - configureFlags = (old.configureFlags or []) ++ [ + configureFlags = (old.configureFlags or [ ]) ++ [ "--enable-load-extension" ]; }); @@ -103,9 +115,10 @@ pkgs.makeWrapper 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 + pkgs.python3 + ] + ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isDarwin [ + pkgs.darwin.cctools ]; buildInputs = [ pkgs.sqlite ]; @@ -130,7 +143,7 @@ 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" + --set LD_LIBRARY_PATH "${pkgs.sqlite.out}/lib:${pkgs.stdenv.cc.libc.out}/lib:${pkgs.stdenv.cc.cc.lib}/lib" ''; meta = with pkgs.lib; { diff --git a/src/llm.ts b/src/llm.ts index 7cccc3fa..366ada89 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -15,8 +15,9 @@ import { type Token as LlamaToken, } from "node-llama-cpp"; import { homedir } from "os"; -import { join } from "path"; -import { existsSync, mkdirSync, statSync, unlinkSync, readdirSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from "fs"; +import { dirname, join } from "path"; +import { accessSync, constants, existsSync, mkdirSync, statSync, unlinkSync, readdirSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from "fs"; +import { createRequire } from "module"; // ============================================================================= // Embedding Formatting Functions @@ -57,6 +58,23 @@ export function formatDocForEmbedding(text: string, title?: string, modelUri?: s return `title: ${title || "none"} | text: ${text}`; } +// ============================================================================= +// Build Writability Check +// ============================================================================= + +const require = createRequire(import.meta.url); + +/** Whether node-llama-cpp can write to its llama/ directory (false on NixOS). */ +function canWriteLlamaDir(): boolean { + try { + const pkgDir = dirname(require.resolve("node-llama-cpp/package.json")); + accessSync(join(pkgDir, "llama"), constants.W_OK); + return true; + } catch { + return false; + } +} + // ============================================================================= // Types // ============================================================================= @@ -618,13 +636,14 @@ export class LlamaCpp implements LLM { private async ensureLlama(allowBuild = true): Promise { if (!this.llama) { const gpuMode = resolveLlamaGpuMode(); + const buildMode = allowBuild && canWriteLlamaDir() ? "autoAttempt" : "never"; const loadLlama = async (gpu: LlamaGpuMode) => await getLlama({ - build: allowBuild ? "autoAttempt" : "never", + build: buildMode, logLevel: LlamaLogLevel.error, gpu, - skipDownload: !allowBuild, + skipDownload: buildMode === "never", }); let llama: Llama;