ENH clean up system dep code

This commit is contained in:
Nathan Dwarshuis 2022-06-14 20:29:44 -04:00
parent efe3eac7c3
commit 9c0a153872
1 changed files with 144 additions and 57 deletions

View File

@ -7,9 +7,9 @@ This is my personal emacs config. It is quite massive. Please use the table of c
- [[#config-structure][config structure]] - [[#config-structure][config structure]]
- [[#continuous-integration][continuous integration]] - [[#continuous-integration][continuous integration]]
- [[#library][library]] - [[#library][library]]
- [[#system-dependencies][system dependencies]]
- [[#external][external]] - [[#external][external]]
- [[#internal][internal]] - [[#internal][internal]]
- [[#system-dependencies][system dependencies]]
- [[#macros][macros]] - [[#macros][macros]]
- [[#functions][functions]] - [[#functions][functions]]
- [[#interactive][interactive]] - [[#interactive][interactive]]
@ -102,60 +102,6 @@ In the root of this directory is a =.github= folder with some simple tests to en
The danger with only having emacs on my daily driver is that I could silently introduce a dependency on some system library, and this may or may not be present when I unpack this config on a different machine. For now, the CI pipeline simply tests whether or not this config will initialize and build correctly on a "bare" system, and also tests if I can pull a list of dependencies using my somewhat hacky [[#system-dependencies][API]] so they can be installed via the package manager. The danger with only having emacs on my daily driver is that I could silently introduce a dependency on some system library, and this may or may not be present when I unpack this config on a different machine. For now, the CI pipeline simply tests whether or not this config will initialize and build correctly on a "bare" system, and also tests if I can pull a list of dependencies using my somewhat hacky [[#system-dependencies][API]] so they can be installed via the package manager.
* library * library
This is code that is used generally throughout the emacs config This is code that is used generally throughout the emacs config
** system dependencies
:PROPERTIES:
:ID: 2dc12a82-cb0c-40f1-ab5a-46d2800e9e53
:END:
#+begin_src emacs-lisp
(defvar nd/required-exes '()
"Running list of executables required to run various configuations.")
(defun nd/require-bin (bin-or-path &optional pkg-type pkg-name)
"Indicate that a binary executable is required.
BIN-OR-PATH is a string indicating the executable name or path to
the executable. PKG-TYPE indicates how BIN-ON-PATH must be
installed (see `nd/required-exes' for available types). PKG-NAME
indicates the package name to install which provides BIN-OR-PATH,
which defaults to BIN-OR-PATH if not given."
(let* ((pt (or pkg-type :pacman))
(name (f-base bin-or-path))
(pn (or pkg-name name))
(new (list name
:full-path (executable-find bin-or-path)
:pkg-type pt
:pkg-name pn)))
(setq nd/required-exes (cons new nd/required-exes))))
(defmacro nd/if-bin (bin then &rest else)
"Execute THEN if BIN exists, otherwise do ELSE."
(declare (indent 1))
(if-let (x (alist-get bin nd/required-exes nil nil #'equal))
`(if ,(plist-get x :full-path) ,then ,@else)
(message "WARNING: executable '%s' must be required" bin)))
(defmacro nd/when-bin (bin &rest body)
"Execute BODY if the program BIN exists."
(declare (indent 1))
`(nd/if-bin ,bin (progn ,@body)
(message "Executable %s not found. Skipping." ,bin)))
(defun nd/verify-required-packages ()
"Verify `nd/required-exes'.
All binaries should be specified once."
(->> (-map #'car nd/required-exes)
(-uniq)
(length)
(equal (length nd/required-exes))))
(defun nd/get-dependencies (keys)
"Return list of all dependencies.
KEYS is a list of keywords that indicate the :pkg-type of
dependencies to return."
(->> nd/required-exes
(--filter (memq (plist-get (cdr it) :pkg-type) keys))
(--map (plist-get it :pkg-name))
(-uniq)))
#+end_src
** external ** external
Some useful external libraries that I use all over the place Some useful external libraries that I use all over the place
*** string manipulation *** string manipulation
@ -208,6 +154,147 @@ Define a path to internal libraries (either things I am developing or external =
(defun nd/expand-lib-directory (path) (defun nd/expand-lib-directory (path)
(f-join user-emacs-directory nd/lib-directory path)) (f-join user-emacs-directory nd/lib-directory path))
#+END_SRC #+END_SRC
** system dependencies
:PROPERTIES:
:ID: 2dc12a82-cb0c-40f1-ab5a-46d2800e9e53
:END:
*** Definitions
#+begin_src emacs-lisp
;; dependency declaration
(defvar nd/required-exes nil
"Running list of executables required to run various configuations.")
(defconst nd/valid-package-types '(:pacman :aur :gem :local))
(defun nd/require-bin (bin-or-path &optional pkg-type pkg-name)
"Indicate that a binary executable is required.
BIN-OR-PATH is a string indicating the executable name or path to
the executable. PKG-TYPE indicates how BIN-ON-PATH must be
installed (see `nd/required-exes' for available types). PKG-NAME
indicates the package name to install which provides BIN-OR-PATH,
which defaults to BIN-OR-PATH if not given."
(let* ((pt (or pkg-type :pacman))
(name (f-base bin-or-path))
(pn (or pkg-name name))
(new (list :full-path (executable-find bin-or-path)
:pkg-type pt
:pkg-name pn)))
(cond
((not (memq pt nd/valid-package-types))
(warn "Invalid dependency type: %s" pt))
((ht-get nd/required-exes name)
(warn "Dependency already required: %s" name))
(t
(ht-set nd/required-exes name new)))))
(defmacro nd/if-bin (bin then &rest else)
"Execute THEN if BIN exists, otherwise do ELSE."
(declare (indent 1))
(if-let (x (ht-get nd/required-exes bin))
`(if ,(plist-get x :full-path) ,then ,@else)
(warn "executable '%s' must be required" bin)))
(defmacro nd/when-bin (bin &rest body)
"Execute BODY if the program BIN exists."
(declare (indent 1))
`(nd/if-bin ,bin (progn ,@body)
(message "Executable %s not found. Skipping." ,bin)))
(defun nd/get-dependencies (keys)
"Return list of all dependencies.
KEYS is a list of keywords that indicate the :pkg-type of
dependencies to return."
(->> (ht-values nd/required-exes)
(--filter (memq (plist-get it :pkg-type) keys))
(--map (plist-get it :pkg-name))
(-uniq)))
;; pacman-specific introspection
(defvar nd/aur-helper nil
"The aur helper command installed on this system.")
(defun nd/find-aur-helper ()
"Return the current aur helper installed (or nil if none)."
;; add more as needed
(let ((helpers (list "yay")))
(-first #'executable-find helpers)))
(defun nd/pacman-dependencies (uninstalled?)
"Return pacman and aur dependencies.
If UNINSTALLED? is non-nil, return only the packages that are not
installed but required by this config."
(let ((ps (nd/get-dependencies '(:pacman)))
(as (nd/get-dependencies '(:aur))))
(if (not uninstalled?) `(,ps ,as)
(let ((is (->> (shell-command-to-string "pacman -Qq")
(s-split "\n"))))
(list (--remove (member it is) ps)
(--remove (member it is) as))))))
(defun nd/shell-test (cmd &rest args)
"Return t if CMD with ARGS succeeds."
(= 0 (apply #'call-process cmd nil nil nil args)))
(defun nd/pacman-pkg-exists (pkg)
"Return t if PKG exists in the pacman repositories."
(nd/shell-test "pacman" "-Ss" (format "^%s$" pkg)))
(defun nd/aur-pkg-exists (pkg)
"Return t if PKG exists in the aur repositories."
;; add more commands as needed
(cond
((equal nd/aur-helper "yay")
(let ((out (->> (format "yay -Ssaq %s" pkg)
(shell-command-to-string)
(s-split "\n"))))
(and (member pkg out) t)))
(t
(warn "No aur helper found"))))
(defun nd/invalid-pacman-pkgs ()
"Return pacman and aur packages that don't exist."
(-let* (((pacman aur) (nd/pacman-dependencies t))
(ps (-remove #'nd/pacman-pkg-exists pacman))
(as (-remove #'nd/aur-pkg-exists aur)))
(list ps as)))
(defun nd/warn-invalid-pacman-deps ()
"Warn user of any invalid pacman/aur packages in this config."
(-let (((pacman aur) (nd/invalid-pacman-pkgs)))
(--each pacman
(message "Pacman package does not exist in any configured repo: %s" it))
(--each aur
(message "Pacman package does not exist in AUR repo: %s" it))))
(defun nd/install-arch-dependencies ()
"Install all missing pacman/aur dependencies."
(cl-flet
((try-install
(what cmd args pkgs)
(if (not pkgs) (message "No %s packages to install" what)
(let ((res (if (apply #'nd/shell-test cmd (append args pkgs))
"Installed"
"Failed to install")))
(message "%s %s packages: %s" res what (s-join ", " pkgs))))))
(-let (((pacman aur) (nd/pacman-dependencies t)))
(try-install "official" "pacman" '("-S") pacman)
(if nd/aur-helper
(let ((aur-args (list "--needed" "--noconfirm" "--norebuild"
"--removemake" "-S")))
(try-install "unofficial" nd/aur-helper aur-args aur))
(message "No aur helper found")))
nil))
#+end_src
*** Setup
#+begin_src emacs-lisp
;; zero this out so reload won't complain about things already present
(setq nd/required-exes (ht-create #'equal)
nd/aur-helper (nd/find-aur-helper))
(nd/warn-invalid-pacman-deps)
#+end_src
** macros ** macros
:PROPERTIES: :PROPERTIES:
:ID: c83dc04a-754a-4ae4-b7da-cad984a7cb18 :ID: c83dc04a-754a-4ae4-b7da-cad984a7cb18
@ -936,7 +1023,7 @@ Also, this seems to have no relation to the =anaconda.el= package for python.
(f-canonical)) (f-canonical))
"Path to conda (which really means mamba) installation.") "Path to conda (which really means mamba) installation.")
(nd/require-bin (f-join nd/conda-home "bin" "conda")) (nd/require-bin (f-join nd/conda-home "bin" "conda") :local)
(nd/when-bin "conda" (nd/when-bin "conda"
(use-package conda (use-package conda
@ -1074,7 +1161,7 @@ Anaconda is much lighter and easier than elpy. Also use ipython instead of the b
(nd/require-bin "ipython") (nd/require-bin "ipython")
(nd/require-bin "flake8") (nd/require-bin "flake8")
(nd/require-bin "pylint") (nd/require-bin "python-pylint")
(use-package python (use-package python
:after flycheck :after flycheck