[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Proof of Concept: Import Emacs' use-packaged packages into Guix' manifes
From: |
Mekeor Melire |
Subject: |
Proof of Concept: Import Emacs' use-packaged packages into Guix' manifest.scm |
Date: |
Sun, 18 Dec 2022 01:54:06 +0000 |
Hello,
I'd like to share some code, thus prove a concept, ask for
feedback, and ask if it could makes sense, under certain
circumstances, to provide similar code in GNU Guix. The code can
be used in a ~manifest.scm~ file (but probably could also be used
with ~guix package -e~ or inside ~guix repl~) where it can be used
to import all ~emacs-*~ packages that are loaded with
~(use-package PACKAGE [...])~ inside an Elisp-file like ~init.el~:
#+begin_src scheme
;; ~/some/manifest.scm
(specifications->manifest
(append
(el/file->specifications "/home/user/.emacs.d/init.el")
(list
"emacs"
"git"
"gnupg"
"mu"
"syncthing")))
#+end_src
It has many severe caveats, including but not limited to:
1. The code uses Guile/Scheme's ~read~ function which interprets
the Elisp-file as Scheme. Thus, the code does not really find out
which packages are loaded, but rather it only looks for the
~(use-package PACKAGE [...])~ pattern of an S-expression. E.g.
something like ~(apply 'use-package foo)~ won't be caught. (I
don't expect Guile's Emacs-Lisp language-feature to be mature
enough to be usable for this.)
2. Not all external packages are loaded with ~use-package~. E.g.
~(require 'foo)~ might be used directly. But it'd be possible to
extend the code to allow several ways/patterns of importing
packages, and maybe also to specify custom patterns.
3. Not all S-expressions that match against ~(use-package
PACKAGE)~ are load external packages. E.g. ~(if nil (use-package
foo))~ won't load anything at all. Also, neither ~(use-package
emacs)~ nor ~(use-package savehist)~ will load anything, because
they ~require~ features that ship with GNU Emacs by default.
That's why the functions in the appended code offer an optional
keyword argument, #:block, in order to block a list packages.
4. The ~manifest.scm~ is not the single source of truth anymore.
I.e. this code compromises the purpose of a manifest. (It's
possible to extend the code such that the Elisp-file's hash can
optionally be verified with given hash.)
Nevertheless, for me, personally, it's pretty neat and handy to
use, because I don't need to maintain the list of emacs-packages
in two places. I also think that it could come pretty handy for
many others, at least in order to initialize their user-profile,
by running something like ~guix package -e '(some-magic
"/home/user/.emacs.d/init.el")'~.
What do you think? Should this go into a separate, private
channel? Into the Guix Cookbook? Into Guix, if so, then probably
with lots of changes? Should it just stay here, in this mailing
list thread? Or do you think this is just a bad idea in general?
I should mention that I go the idea for this code from the
Nix-Community's Emacs-Overlay which offers a similar feature; as
described in their README as follows:
#+begin_src nix
#
https://github.com/nix-community/emacs-overlay#extra-library-functionality
{
environment.systemPackages = [
(emacsWithPackagesFromUsePackage {
# Your Emacs config file. Org mode babel files are also
# supported.
# NB: Config files cannot contain unicode characters, since
# they're being parsed in nix, which lacks unicode
# support.
# config = ./emacs.org;
config = ./emacs.el;
# ...
})]}
#+end_src
Here's the code. Please show mercy, it's my first real Scheme
code:
(define-module (manifest)
#:autoload (gnu packages) (specifications->manifest)
#:autoload (srfi srfi-1) (drop fold-right)
#:autoload (ice-9 textual-ports) (get-string-all))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; LIBRARY
(define el/default-block
;; "Default list of packages, as symbols, which will be blocked. It's a subset
;; default features provided by GNU Emacs."
'(emacs hl-line outline savehist))
(define* (el/file->specifications content-f #:key (block el/default-block))
"Given the file-path CONTENT-F, interprete it as an Emacs-Lisp source-code
and extract a list of package-names, as strings, that have been loaded using
(use-package PACKAGE [...])."
(call-with-input-file content-f
(lambda (input) (el/port->specifications input #:block block))))
(define* (el/string->specifications content-s #:key (block el/default-block))
"Given a string CONTENT-S, interprete it as an Emacs-Lispsource-code and
extract a list of package-names, as strings, that have been loaded using
(use-package PACKAGE [...])."
(call-with-input-string content-s
(lambda (input) (el/port->specifications input #:block block))))
(define* (el/port->specifications content-p #:key (block el/default-block))
"Given the input-port CONTENT-P, interprete it as an Emacs-Lisp source-code
and extract a list of package-names, as strings, that have been loaded using
(use-package PACKAGE [...])."
(let*
((content-string-or-eof
(get-string-all content-p))
(content-string
(if
(eof-object? content-string-or-eof)
""
content-string-or-eof))
;; let's wrap the file into a (quote ...) so that a single invocation of
~read~ will read the whole file
(quoted-content-string
(string-append "(quote " content-string "\n)"))
(content-expression
(with-input-from-string quoted-content-string read)))
(map
(lambda (package-string) (string-append "emacs-" package-string))
(map
symbol->string
(filter
(lambda (package-expression) (not (member package-expression block)))
(el/find-used-packages content-expression (list)))))))
(define (el/find-used-packages expression accumulator)
"Given a quoted expression EXPRESSION and an accumulator ACCUMULATOR,
being a list of package names, add all package-names into ACCUMULATOR, that
have been loaded with (use-package PACKAGE [...]), and return that possibly
expanded list."
(cond
;; in case of no s-expression, return accumulator
((not (list? (peek expression))) accumulator)
;; in case of an empty s-expression, (), return accumulator
((nil? expression) accumulator)
;; in case of an s-expression with a single element, (X), walk into that
element
((nil? (cdr expression))
(el/find-used-packages (car expression) accumulator))
;; in case of an use-package s-expression, (use-package PACKAGE [...]), add
PACKAGE to the accumulator and also add those elements which possibly result
from walking through the remaining elements
((and
(eq? (car expression) 'use-package)
(symbol? (cadr expression)))
(fold-right
el/find-used-packages
(let
((package (cadr expression)))
(if
(member package accumulator)
accumulator
(cons package accumulator)))
(drop expression 2)))
;; in case of a s-expression with at least two elements, (X Y [...]), walk
through all of them
(#t
(fold-right
el/find-used-packages
accumulator
expression))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MANIFEST
(specifications->manifest
(append
(el/file->specifications "/home/user/.emacs.d/configuration.el" #:block
;; block some packages as they won't be loaded on this host:
(cons*
'gist
'guix-emacs
'hide-lines
'json
'kubernetes
'vertico-directory
el/default-block))
(list
"emacs"
"git"
"isync"
"mu")))