nixd icon indicating copy to clipboard operation
nixd copied to clipboard

Infer flake.nix as the source of nixpkgs

Open psionic-k opened this issue 8 months ago • 4 comments

I realized my editor configuration (this may be used to refine the existing Emacs configuration) is dynamically finding the Flake to use. When most of the time I'm editing a flake.nix, this actually makes little sense.

  ;; Thanks Mr. Purcell
  ;; https://github.com/purcell/emacs.d/blob/master/lisp/init-nix.el
  ;;
  ;; we can ignore the use of nix-ts-mode, which is just tree sitter grammar
  (use-package nix-ts-mode
    :ensure (nix-ts-mode
             :fetcher github
             :repo "remi-gelinas/nix-ts-mode")

    :init (add-to-list 'auto-mode-alist '("\\.nix\\'" . nix-ts-mode))
    :hook (nix-ts-mode . eglot-ensure)
    :config

    ;; Here is the informative bit.  If the flake.nix exists, that is treated as the location
    ;; for the nixpkgs inputs.
    (defun pmx--project-flake-path (_)
      (let ((flake-path (expand-file-name "flake.nix" (projectile-project-root))))
        (if (file-exists-p flake-path)
            `("nixd"
              :initializationOptions
              (:nixpkgs (:expr ,(format "import (builtins.getFlake
      \"%s\").inputs.nixpkgs { }" flake-path))))
          '("nixd"))))

    ;; After eglot is loaded, we add nixd, with this dynamic configuration function, to its settings
    (let ((nix-settings
           '((nix-ts-mode) . #'pmx--project-flake-path)))
      (with-eval-after-load 'eglot
        (add-to-list 'eglot-server-programs nix-settings))))

Why not just decide this when editing a file called flake.nix and infer that it has inputs and that those inputs define what will be used within the file?

psionic-k avatar Apr 01 '25 01:04 psionic-k

Why not just decide this when editing a file called flake.nix and infer that it has inputs and that those inputs define what will be used within the file?

Because a nixd user may not have just one flake.nix; for instance, when editing their own flake (e.g., for NixOS configuration files), they might use version 1 of nixpkgs, while in another project (e.g., niri), which also has a flake.nix, that flake comes with version 2 of nixpkgs. The user might want to always point to and use version 1 of nixpkgs, and may prefer not to fetch version 2 of nixpkgs at all.

inclyc avatar Apr 01 '25 04:04 inclyc

We can still override this via the language server settings. The suggestion is only for the case where nixpkgs is not passed as a setting and the file being edited is flake.nix and defines nixpkgs as an input

psionic-k avatar Apr 01 '25 05:04 psionic-k

I usually set a FLAKE variable in my devShells, load those with direnv and than use this expression to load the flake: (builtins.getFlake (builtins.getEnv "FLAKE")). This works quite well with multiple flake repositories.

LazyStability avatar Apr 30 '25 09:04 LazyStability

use this expression to load the flake: (builtins.getFlake (builtins.getEnv "FLAKE")).

I took some inspiration and wrote a nix expression that finds the current flake by walking up the current directory path.

This works best if you can save the nix expression to a file in the nix store and reference that in your nixd config:

https://github.com/MattSturgeon/nix-config/commit/b8aa42d6c01465949ef5cd9d4dc086d4eaa36793

Nix expression

let
  self = "TODO: Path to main/global flake";
  system = builtins.currentSystem;

  # Reimplementation of `lib.lists.dropEnd 1` using builtins
  dropLast =
    list:
    let
      len = builtins.length list;
      dropped = builtins.genList (builtins.elemAt list) (len - 1);
    in
    if list == [ ] || len == 1 then [ ] else dropped;

  # Walk up the directory path, looking for a flake.nix file
  # Called with an absolute filepath
  findFlake =
    dir:
    let
      isPart = part: builtins.isString part && part != "" && part != ".";
      parts = builtins.filter isPart (builtins.split "/+" dir);
    in
    findFlake' parts;

  # Underlying impl of findFlake
  # Called with a list path instead of a string path
  findFlake' =
    parts:
    let
      dir = "/" + builtins.concatStringsSep "/" parts;
      files = builtins.readDir dir;
      isFlake = files."flake.nix" or null == "regular";
      parent = dropLast parts;
    in
    if parts == [ ] then
      null
    else if isFlake then
      dir
    else
      findFlake' parent;

  # Path to the local flake, or null
  path = findFlake (builtins.getEnv "PWD");
in
{
  inherit system self path;

  local = if path == null then null else builtins.getFlake path;
  global = builtins.getFlake self;
}

Then you can import that file into your nixd expressions:

nixd settings

init.lua = /* lua */ ''
{
  nixd = {
    diagnostic = {
      suppress = { "sema-escaping-with", "var-bind-to-this" }
    },
    nixpkgs = {
      expr = "with import ${./nixd-expr.nix}; import (if local ? lib.version then local else local.inputs.nixpkgs or global.inputs.nixpkgs) { }"
    },
    options = {
      ["flake-parts"] = {
	expr = "with import ${./nixd-expr.nix}; local.debug.options or global.debug.options"
      },
      ["home-manager"] = {
	expr = "with import ${./nixd-expr.nix}; global.nixosConfigurations.desktop.options.home-manager.users.type.getSubOptions [ ]"
      },
      nixos = {
	expr = "with import ${./nixd-expr.nix}; global.nixosConfigurations.desktop.options"
      },
      nixvim = {
	expr = "with import ${./nixd-expr.nix}; global.nixvimConfigurations.''${system}.default.options"
      }
    }
  }
}
''

MattSturgeon avatar May 12 '25 17:05 MattSturgeon