575 lines
20 KiB
EmacsLisp
575 lines
20 KiB
EmacsLisp
;;; nix-flake.el --- Transient interface to Nix flake commands -*- lexical-binding: t -*-
|
|
|
|
;; Keywords: nix, languages, tools, unix
|
|
;; Package-Requires: ((emacs "27.1") (transient "0.3"))
|
|
;; Homepage: https://github.com/NixOS/nix-mode
|
|
|
|
;;; Commentary:
|
|
|
|
;; This library provides transient interface to experimental commands in Nix.
|
|
;; See the Nix manual for more information available at
|
|
;; https://nixos.org/manual/nix/unstable/command-ref/experimental-commands.html
|
|
|
|
;;; Code:
|
|
|
|
(require 'nix)
|
|
(require 'transient)
|
|
|
|
(defgroup nix-flake nil
|
|
"Nix flake commands."
|
|
:group 'nix)
|
|
|
|
;;;; Custom variables
|
|
|
|
(defcustom nix-flake-init-post-action 'open-flake-nix
|
|
"Action to run after successfully initializing a flake.
|
|
|
|
This action is run after a flake is successfully initialized by
|
|
`nix-flake-init` (or generally `nix-flake-dispatch`).
|
|
|
|
You can also specify a function, which should take no arguments.
|
|
It is called in the directory of the flake."
|
|
:type '(choice (const :tag "Open flake.nix" open-flake-nix)
|
|
(const :tag "Do nothing" nil)
|
|
(function :tag "User-defined function")))
|
|
|
|
(defcustom nix-flake-add-to-registry t
|
|
"Whether to add a new flake to registry.
|
|
|
|
When this variable is non-nil, every flake reference from the
|
|
interactive input is added to the flake registry, unless it is
|
|
already registered in either the user or the global registry."
|
|
:type 'boolean)
|
|
|
|
;;;; Transient classes
|
|
|
|
;;;;; flake-ref
|
|
|
|
(defclass nix-flake-ref-variable (transient-variable)
|
|
((constant-value :initarg :constant-value :initform nil)
|
|
(on-change :initarg :on-change)
|
|
(reader :initarg :reader :initform nil)))
|
|
|
|
(cl-defmethod transient-init-value ((_obj nix-flake-ref-variable))
|
|
"Set the initial value of the object.")
|
|
|
|
(cl-defmethod transient-infix-read ((obj nix-flake-ref-variable))
|
|
"Determine the new value of the infix object OBJ."
|
|
(if-let (value (oref obj constant-value))
|
|
(if (symbolp value)
|
|
(symbol-value value)
|
|
value)
|
|
(if-let (reader (oref obj reader))
|
|
(funcall reader "Flake directory: " (oref obj value))
|
|
(nix-flake--read-flake-ref nil (oref obj value)))))
|
|
|
|
(cl-defmethod transient-infix-set ((obj nix-flake-ref-variable) value)
|
|
"Set the value of infix object OBJ to VALUE."
|
|
(oset obj value value)
|
|
(when-let (func (oref obj on-change))
|
|
(funcall func value)))
|
|
|
|
(cl-defmethod transient-format-value ((_obj nix-flake-ref-variable))
|
|
"Format the object's value for display and return the result."
|
|
;; Don't show the value
|
|
"")
|
|
|
|
;;;; Utility functions
|
|
|
|
(defun nix-flake--to-list (x)
|
|
"If X is not a list, make a singleton list containing it."
|
|
(if (listp x)
|
|
x
|
|
(list x)))
|
|
|
|
;;;; Registry
|
|
;; Maybe we'll move these functions to a separate library named nix-registry.el.
|
|
|
|
(defun nix-flake--registry-list ()
|
|
"Return a list of entries from the registry."
|
|
(cl-flet
|
|
((split-entry
|
|
(s)
|
|
(split-string s "[[:space:]]+")))
|
|
(thread-last (nix--process-lines "registry" "list")
|
|
(mapcar #'split-entry))))
|
|
|
|
(defun nix-flake--registry-refs ()
|
|
"Return a list of flake refs in the registry."
|
|
(thread-last (nix-flake--registry-list)
|
|
;; I don't know if I should include flakes in the
|
|
;; system registry. It's ugly to display full
|
|
;; checksums, so I won't include them for now.
|
|
(cl-remove-if-not (pcase-lambda (`(,type . ,_))
|
|
(member type '("user" "global"))))
|
|
(mapcar (lambda (cells)
|
|
;; Both references and referees are included in the output.
|
|
;; It may be better to pick only one and show others as
|
|
;; decoration, e.g. using marginalia, but it is not supported
|
|
;; for now.
|
|
(list (nth 1 cells)
|
|
(nth 2 cells))))
|
|
(flatten-list)))
|
|
|
|
(defun nix-flake--registry-add-1 (flake-ref)
|
|
"Add FLAKE-REF to the registry with a new name."
|
|
(let ((name (read-string (format-message "Enter the registry name for %s: "
|
|
flake-ref))))
|
|
(unless (or (not name)
|
|
(string-empty-p name))
|
|
(start-process "nix registry add" "*nix registry add*"
|
|
nix-executable
|
|
"registry" "add" name flake-ref))))
|
|
|
|
;; This argument complies the standard reader interface of transient
|
|
;; just in case, but it may not be necessary.
|
|
(defun nix-flake--read-flake-ref (&optional prompt initial-input history)
|
|
"Select a flake from the registry.
|
|
|
|
For PROMPT, INITIAL-INPUT, and HISTORY, see the documentation of
|
|
readers in transient.el."
|
|
(let* ((registered-flakes (nix-flake--registry-refs))
|
|
(input (string-trim
|
|
(completing-read (or prompt "Flake URL: ")
|
|
registered-flakes
|
|
nil nil nil history initial-input))))
|
|
(prog1 input
|
|
(when (and nix-flake-add-to-registry
|
|
(not (member input registered-flakes)))
|
|
(nix-flake--registry-add-1 input)))))
|
|
|
|
;;;; nix-flake command
|
|
|
|
;;;;; Variables
|
|
|
|
(defvar nix-flake-ref nil)
|
|
|
|
;;;;; Setting the flake
|
|
(transient-define-infix nix-flake:from-registry ()
|
|
:class 'nix-flake-ref-variable
|
|
:on-change
|
|
(lambda (flake-ref)
|
|
(nix-flake--set-flake flake-ref :remote t)
|
|
(transient-update))
|
|
:description "Select a flake from the registry")
|
|
|
|
(transient-define-infix nix-flake:flake-directory ()
|
|
:class 'nix-flake-ref-variable
|
|
:reader 'nix-flake--read-directory
|
|
:on-change
|
|
(lambda (flake-ref)
|
|
(nix-flake--set-flake flake-ref)
|
|
(transient-update))
|
|
:description "Select a directory")
|
|
|
|
(defun nix-flake--read-directory (prompt &optional initial-input _history)
|
|
"Select a directory containing a flake.
|
|
|
|
For PROMPT and INITIAL-INPUT, see the documentation of transient.el."
|
|
(let ((input (string-remove-suffix "/" (read-directory-name prompt initial-input nil t))))
|
|
(prog1 (expand-file-name input)
|
|
(unless (file-exists-p (expand-file-name "flake.nix" input))
|
|
(user-error "The selected directory does not contain flake.nix"))
|
|
(when (and nix-flake-add-to-registry
|
|
(not (member (concat "path:" input)
|
|
(nix-flake--registry-refs))))
|
|
(nix-flake--registry-add-1 input)))))
|
|
|
|
;;;;; --update-input
|
|
|
|
(defclass nix-flake--update-input-class (transient-option)
|
|
())
|
|
|
|
(transient-define-infix nix-flake-arg:update-input ()
|
|
:class 'nix-flake--update-input-class
|
|
:argument "--update-input"
|
|
:reader 'nix-flake--read-input-path
|
|
:prompt "Input: "
|
|
:description "Update a specific flake path")
|
|
|
|
(cl-defmethod transient-format-value ((obj nix-flake--update-input-class))
|
|
"Format --update-input arguments from OBJ."
|
|
(let ((value (oref obj value)))
|
|
(propertize (concat (oref obj argument)
|
|
(when value
|
|
(concat " " value)))
|
|
'face (if value
|
|
'transient-value
|
|
'transient-inactive-value))))
|
|
|
|
(cl-defmethod transient-infix-value ((obj nix-flake--update-input-class))
|
|
"Return the value of the suffix object OBJ."
|
|
(when-let ((value (oref obj value)))
|
|
(list (oref obj argument) value)))
|
|
|
|
(defun nix-flake--input-names ()
|
|
"Return a list of inputs to the flake."
|
|
(thread-last (nix--process-json "flake" "info" nix-flake-ref "--json")
|
|
(alist-get 'locks)
|
|
(alist-get 'nodes)
|
|
(alist-get 'root)
|
|
(alist-get 'inputs)
|
|
(mapcar #'cdr)))
|
|
|
|
(defun nix-flake--read-input-path (prompt initial-input _history)
|
|
"Read an input name of a flake from the user.
|
|
|
|
For PROMPT and INITIAL-INPUT, see the documentation of :reader in
|
|
transient.el."
|
|
(completing-read prompt (nix-flake--input-names)
|
|
nil nil initial-input))
|
|
|
|
;;;;; Attribute names
|
|
|
|
(defvar nix-flake-outputs nil)
|
|
|
|
(defun nix-flake-system-attribute-names (types)
|
|
"Return a list of output attributes of particular TYPES."
|
|
(let ((system (intern (nix-system))))
|
|
(thread-last nix-flake-outputs
|
|
(mapcar (pcase-lambda (`(,type . ,alist))
|
|
(when (memq type types)
|
|
(mapcar #'car (alist-get system alist)))))
|
|
(apply #'append)
|
|
(cl-remove-duplicates)
|
|
(mapcar #'symbol-name))))
|
|
|
|
(defun nix-flake--run-attribute-names ()
|
|
"Return possible attribute names for run command."
|
|
(nix-flake-system-attribute-names '(apps packages)))
|
|
|
|
(defun nix-flake--build-attribute-names ()
|
|
"Return possible attribute names for build command."
|
|
(nix-flake-system-attribute-names '(packages)))
|
|
|
|
(defun nix-flake--default-run-p ()
|
|
"Return non-nil if there is the default derivation for run command."
|
|
(not (null (nix-flake-system-attribute-names '(defaultApp defaultPackage)))))
|
|
|
|
(defun nix-flake--default-build-p ()
|
|
"Return non-nil if there is the default derivation for build command."
|
|
(not (null (nix-flake-system-attribute-names '(defaultPackage)))))
|
|
|
|
;;;;; Building command lines
|
|
|
|
(defun nix-flake--options ()
|
|
"Return a list of options for `nix-flake-dispatch'."
|
|
(flatten-list (transient-args 'nix-flake-dispatch)))
|
|
|
|
(defun nix-flake--command (subcommand options flake-ref)
|
|
"Build a command line for a Nix subcommand.
|
|
|
|
SUBCOMMAND is a string or a list of strings which is a subcommand of Nix.
|
|
|
|
OPTIONS is a list of options appended after FLAKE-REF.
|
|
|
|
COMMAND-ARGUMENTS is extra arguments to the
|
|
command after the flake reference."
|
|
(concat nix-executable
|
|
" "
|
|
(mapconcat #'shell-quote-argument
|
|
`(,@(nix-flake--to-list subcommand)
|
|
,flake-ref
|
|
,@options)
|
|
" ")))
|
|
|
|
(defun nix-flake--installable-command (subcommand options flake-ref attribute
|
|
&optional extra-arguments)
|
|
"Build a command line for a Nix subcommand.
|
|
|
|
This is like `nix-flake--command', but for a subcommand which
|
|
takes an installable as an argument. See the user manual of Nix
|
|
for what installable means.
|
|
|
|
SUBCOMMAND, OPTIONS, and FLAKE-REF are the same as in the
|
|
function. ATTRIBUTE is the name of a package, app, or anything
|
|
that refers to a derivation in the flake. It must be a string
|
|
that is concatenated with the sharp symbol in the installable
|
|
reference.
|
|
|
|
EXTRA-ARGUMENTS is a list of strings passed to the Nix command
|
|
after \"--\". Note that some commands such as \"nix build\" do
|
|
not take the extra arguments."
|
|
(concat nix-executable
|
|
" "
|
|
(mapconcat #'shell-quote-argument
|
|
`(,@(nix-flake--to-list subcommand)
|
|
,(if attribute
|
|
(concat flake-ref "#" attribute)
|
|
flake-ref)
|
|
,@options)
|
|
" ")
|
|
(if extra-arguments
|
|
(concat " -- " extra-arguments)
|
|
"")))
|
|
|
|
;;;;; Individual subcommands
|
|
|
|
(defun nix-flake-run-attribute (options flake-ref attribute command-args)
|
|
"Run an app in the current flake.
|
|
|
|
OPTIONS and FLAKE-REF are the same as in other Nix commands.
|
|
ATTRIBUTE is the name of a package or app in the flake, and
|
|
COMMAND-ARGS is an optional list of strings passed to the
|
|
application."
|
|
(interactive (list (nix-flake--options)
|
|
nix-flake-ref
|
|
(completing-read "Nix app/package: "
|
|
(nix-flake--run-attribute-names))
|
|
nil))
|
|
(compile (nix-flake--installable-command "run" options flake-ref attribute
|
|
command-args)))
|
|
|
|
(defun nix-flake-run-default (options flake-ref command-args)
|
|
"Run the default app or package in the current flake.
|
|
|
|
For OPTIONS, FLAKE-REF, and COMMAND-ARGS, see the documentation of
|
|
`nix-flake-run-attribute'."
|
|
(interactive (list (nix-flake--options)
|
|
nix-flake-ref
|
|
nil))
|
|
(compile (nix-flake--installable-command "run" options flake-ref nil
|
|
command-args)))
|
|
|
|
(defun nix-flake-build-attribute (options flake-ref attribute)
|
|
"Build a derivation in the current flake.
|
|
|
|
For OPTIONS, FLAKE-REF, and ATTRIBUTE, see the documentation of
|
|
`nix-flake-run-attribute'."
|
|
(interactive (list (nix-flake--options)
|
|
nix-flake-ref
|
|
(completing-read "Nix package: "
|
|
(nix-flake--build-attribute-names))))
|
|
(compile (nix-flake--installable-command "build" options flake-ref attribute)))
|
|
|
|
(defun nix-flake-build-default (options flake-ref)
|
|
"Build the default package in the current flake.
|
|
|
|
|
|
For OPTIONS and FLAKE-REF, see the documentation of
|
|
`nix-flake-run-attribute'."
|
|
(interactive (list (nix-flake--options)
|
|
nix-flake-ref))
|
|
(compile (nix-flake--installable-command "build" options flake-ref nil)))
|
|
|
|
(defun nix-flake-check (options flake-ref)
|
|
"Check the flake.
|
|
|
|
For OPTIONS and FLAKE-REF, see the documentation of
|
|
`nix-flake-run-attribute'."
|
|
(interactive (list (nix-flake--options) nix-flake-ref))
|
|
(compile (nix-flake--command '("flake" "check") options flake-ref)))
|
|
|
|
(defun nix-flake-lock (options flake-ref)
|
|
"Create missing lock file entries.
|
|
|
|
For OPTIONS and FLAKE-REF, see the documentation of
|
|
`nix-flake-run-attribute'."
|
|
(interactive (list (nix-flake--options) nix-flake-ref))
|
|
(compile (nix-flake--command '("flake" "lock") options flake-ref)))
|
|
|
|
(defun nix-flake-update (options flake-ref)
|
|
"Update the lock file.
|
|
|
|
For OPTIONS and FLAKE-REF, see the documentation of
|
|
`nix-flake-run-attribute'."
|
|
(interactive (list (nix-flake--options) nix-flake-ref))
|
|
(compile (nix-flake--command '("flake" "update") options flake-ref)))
|
|
|
|
;;;###autoload (autoload 'nix-flake-dispatch "nix-flake" nil t)
|
|
(transient-define-prefix nix-flake-dispatch (flake-ref &optional remote)
|
|
"Run a command on a Nix flake."
|
|
:value '("--print-build-logs")
|
|
[:description
|
|
nix-flake--description
|
|
("=r" nix-flake:from-registry)
|
|
("=d" nix-flake:flake-directory)]
|
|
["Arguments"
|
|
("-i" "Allow access to mutable paths and repositories" "--impure")
|
|
("-ui" nix-flake-arg:update-input)
|
|
("-nu" "Do not allow any updates to the flake's lock file" "--no-update-lock-file")
|
|
("-cl" "Commit changes to the flake's lock file" "--commit-lock-file")
|
|
("-L" "Print build logs" "--print-build-logs")]
|
|
["Installable commands"
|
|
("r" "Run attribute" nix-flake-run-attribute)
|
|
("R" "Run default" nix-flake-run-default :if nix-flake--default-run-p)
|
|
("b" "Build attribute" nix-flake-build-attribute)
|
|
("B" "Build default" nix-flake-build-default :if nix-flake--default-build-p)]
|
|
["Flake commands"
|
|
("c" "flake check" nix-flake-check)
|
|
("l" "flake lock" nix-flake-lock)
|
|
("u" "flake update" nix-flake-update)]
|
|
(interactive (list (convert-standard-filename default-directory)))
|
|
(nix-flake--set-flake flake-ref :remote remote)
|
|
(transient-setup 'nix-flake-dispatch))
|
|
|
|
(cl-defun nix-flake--set-flake (flake-ref &key remote)
|
|
"Set the flake of the transient interface.
|
|
|
|
FLAKE-REF and REMOTE should be passed down from `nix-flake-dispatch'."
|
|
(setq nix-flake-ref flake-ref)
|
|
(setq nix-flake-outputs
|
|
(if remote
|
|
(nix--process-json "flake" "show" "--json" nix-flake-ref)
|
|
(let ((default-directory flake-ref))
|
|
(nix--process-json "flake" "show" "--json")))))
|
|
|
|
(defun nix-flake--description ()
|
|
"Describe the current flake."
|
|
(concat "Flake: " nix-flake-ref))
|
|
|
|
;; A wrapper function for ensuring existence of flake.nix and flake.lock
|
|
;; in the project directory.
|
|
;;;###autoload
|
|
(cl-defun nix-flake (dir &key flake-ref)
|
|
"Dispatch a transient interface for Nix commands.
|
|
|
|
DIR is a directory on the file system in which flake.nix resides.
|
|
|
|
Alternatively, you can specify FLAKE-REF which follows the syntax
|
|
of flake-url. It can refer to a remote url, a local file path, or
|
|
whatever supported by Nix."
|
|
(interactive (pcase current-prefix-arg
|
|
('(4) (list nil :flake-ref (nix-flake--read-flake-ref)))
|
|
('(16) (if nix-flake-ref
|
|
(list nil :flake-ref nix-flake-ref)
|
|
(user-error "Last flake is unavailable")))
|
|
(_ (list default-directory))))
|
|
(cl-assert (or (and (stringp dir) (file-directory-p dir))
|
|
flake-ref)
|
|
nil
|
|
"DIR or FLAKE-REF must be specified")
|
|
(cond
|
|
(flake-ref
|
|
(nix-flake-dispatch flake-ref t))
|
|
((file-exists-p (expand-file-name "flake.lock" dir))
|
|
(nix-flake-dispatch (nix-flake--directory-ref dir)))
|
|
((file-exists-p (expand-file-name "flake.nix" dir))
|
|
(message "You have not created flake.lock yet, so creating it...")
|
|
(let ((default-directory dir))
|
|
(nix-flake--command '("flake" "lock") nil
|
|
(nix-flake--directory-ref dir))))
|
|
(t
|
|
(nix-flake-init-dispatch))))
|
|
|
|
(defun nix-flake--directory-ref (dir)
|
|
"Return the flake ref for a local DIR."
|
|
(expand-file-name dir))
|
|
|
|
;;;; nix flake init
|
|
|
|
;;;;; Setting the template repository
|
|
|
|
(defvar nix-flake-template-repository nil
|
|
"Flake reference to the current template sets.")
|
|
|
|
(defvar nix-flake-template-name nil
|
|
"Attribute name of the last used template.")
|
|
|
|
(defun nix-flake--init-source ()
|
|
"Describe the current template repository for init command."
|
|
(format "Template repository: %s" nix-flake-template-repository))
|
|
|
|
(transient-define-infix nix-flake-init:from-registry ()
|
|
:class 'nix-flake-ref-variable
|
|
:variable 'nix-flake-template-repository
|
|
:description "Select from the registry")
|
|
|
|
(transient-define-infix nix-flake-init:default-templates ()
|
|
:class 'nix-flake-ref-variable
|
|
:variable 'nix-flake-template-repository
|
|
:constant-value "flake:templates"
|
|
:description "Use the default template set")
|
|
|
|
;;;;; Running the command
|
|
|
|
(defun nix-flake--init (flake-ref template-name)
|
|
"Initialize a flake from a template.
|
|
|
|
FLAKE-REF must be a reference to a flake which contains the
|
|
template, TEMPLATE-NAME is the name of the template."
|
|
;; Save the selection state for later use.
|
|
(setq nix-flake-template-repository flake-ref
|
|
nix-flake-template-name template-name)
|
|
(let ((proc (start-process "nix flake init"
|
|
"*nix flake init*"
|
|
nix-executable
|
|
"flake"
|
|
"init"
|
|
"-t"
|
|
(concat flake-ref "#" template-name))))
|
|
(set-process-sentinel proc
|
|
(lambda (process _event)
|
|
(when (eq 'exit (process-status process))
|
|
(if (= 0 (process-exit-status process))
|
|
(nix-flake-init-post-action)
|
|
(message "Returned non-zero from nix flake init")))))
|
|
proc))
|
|
|
|
(defun nix-flake-init-post-action ()
|
|
"Perform an post-process action depending on the configuration.
|
|
|
|
See `nix-flake-init-post-action' variable for details."
|
|
(pcase nix-flake-init-post-action
|
|
('open-flake-nix
|
|
(find-file "flake.nix"))
|
|
((pred functionp)
|
|
(funcall nix-flake-init-post-action))))
|
|
|
|
;;;;; Selecting a template
|
|
|
|
(defun nix-flake--templates (flake-ref)
|
|
"Return a list of templates in FLAKE-REF."
|
|
(thread-last (nix--process-json "flake" "show" "--json" flake-ref)
|
|
(alist-get 'templates)
|
|
(mapcar #'car)
|
|
(mapcar #'symbol-name)))
|
|
|
|
;; It might be better to use `transient-define-suffix', but I don't know for
|
|
;; sure.
|
|
(defun nix-flake-init-select-template ()
|
|
"Select a template and initialize a flake."
|
|
(interactive)
|
|
(let* ((flake-ref (or nix-flake-template-repository
|
|
(nix-flake--read-flake-ref "Template repository: ")))
|
|
(template-name (completing-read
|
|
(format-message "Select a template from %s: " flake-ref)
|
|
(nix-flake--templates flake-ref))))
|
|
(nix-flake--init flake-ref template-name)))
|
|
|
|
;;;;; The transient interface
|
|
|
|
;;;###autoload (autoload 'nix-flake-init "nix-flake" nil t)
|
|
(transient-define-prefix nix-flake-init-dispatch (&optional flake-ref)
|
|
"Scaffold a project from a template."
|
|
[:description "Initialize a flake"]
|
|
[:description
|
|
nix-flake--init-source
|
|
("=r" nix-flake-init:from-registry)
|
|
("=d" nix-flake-init:default-templates)]
|
|
["Initialize a flake"
|
|
("t" "Select template" nix-flake-init-select-template)]
|
|
(interactive (list nil))
|
|
(when flake-ref
|
|
(setq nix-flake-template-repository flake-ref))
|
|
(transient-setup 'nix-flake-init-dispatch))
|
|
|
|
;;;###autoload
|
|
(defun nix-flake-init ()
|
|
"Run \"nix flake init\" command via a transient interface."
|
|
(interactive)
|
|
(let* ((root (locate-dominating-file default-directory ".git"))
|
|
(default-directory
|
|
(if (and root
|
|
(not (file-equal-p root default-directory))
|
|
(yes-or-no-p (format-message
|
|
"The directory %s is not the repository root. Change to %s?"
|
|
default-directory root)))
|
|
root
|
|
default-directory)))
|
|
(if (file-exists-p "flake.nix")
|
|
(user-error "The directory already contains a flake")
|
|
(nix-flake-init-dispatch))))
|
|
|
|
(provide 'nix-flake)
|
|
;;; nix-flake.el ends here
|