guix-devel
[Top][All Lists]
Advanced

[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")))

reply via email to

[Prev in Thread] Current Thread [Next in Thread]