From 6e0d73abf527901df080f0f5d7d272722d89c87a Mon Sep 17 00:00:00 2001 From: Max Nikulin Date: Wed, 3 May 2023 18:39:49 +0700 Subject: [PATCH] epm.el: A CLI tool for package.el * mk/epm.el: A helper to install build time dependencies from ELPA. --- mk/epm.el | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100755 mk/epm.el diff --git a/mk/epm.el b/mk/epm.el new file mode 100755 index 000000000..2816702bb --- /dev/null +++ b/mk/epm.el @@ -0,0 +1,243 @@ +#!/bin/sh +":"; # -*- mode: emacs-lisp; lexical-binding: t; -*- +":"; exec emacs --script "$0" "$@" +;;; epm.el --- Emacs package management helper for Org Mode + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; Author: Max Nikulin +;; Created: 2 May 2023 +;; Keywords: maint, tools +;; Package-Requires: ((emacs "26.1")) +;; URL: https://orgmode.org +;; Version: 0.1 + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: +;; +;; epm.el is an attempt to create a tool that allows simple package +;; management outside of interactive Emacs session. Its purpose +;; is dependency management for make based workflow for building +;; and testing and for continuous integration (CI) systems. +;; It install not available yet packages from ELPA. +;; +;; Before compiling or running Org mode uncompiled it is necessary +;; to install dependencies. If libraries are already available +;; in your `load-path' then the following commands should be no-op. +;; +;; As a user who prefers to use Org mode version from git +;; likely you would prefer default ~/emacs.d/elpa directory +;; for installed packages +;; +;; mk/epm.el -q install compat +;; +;; If you are a developer and you need to separate environment from +;; main Emacs configuration, you may choose some alternative +;; package directory and use %e substitution for Emacs version. +;; +;; export EPMDIR="$HOME/.cache/epm/emacs-%e" +;; /path/to/emacs-src/emacs -q --script mk/epm.el install compat +;; +;; or +;; +;; /path/to/emacs-src/emacs -q --script mk/epm.el \ +;; --epm-dir "$HOME/.cache/epm/emacs-%e" install compat +;; +;; Of course, you should specify location to overriden `package-user-dir' +;; in local.mk. +;; +;; When a bug is suspected, it is better to install dependencies +;; e.g. to TMPDIR to avoid issues with content of user init directory. +;; +;; If you prefer to avoid ELPA packages and this script, you still have +;; --directory/-L option and EMACSLOADPATH environment variable +;; to specify where required libraries may be loaded. +;; +;; Since required dependency may be installed e.g. as elpa-compat Debian +;; package, it is not recommended to use --quick/-Q or --no-site-lisp/-nsl +;; options, prefer --no-user-init/-q instead unless you suspect some +;; issue with site-lisp directories. +;; +;; Limitations: +;; - package.el allows to check if minimum version requirement is satisfied +;; for a package, but I have not found API to check it for a library from +;; `load-path'. +;; - Upgrading of a package is not implemented and I am unsure if convenient +;; API exists. +;; - Ideally istead of library list it should be possible to specify .el file +;; and dependencies should be taken from the Package-Requires header. +;; +;; To get list of libraries that are not available run +;; +;; mk/epm.el -q missing compat +;; +;; Non-zero exit code means missing dependencies, its list is printed to stdout. +;; To check which file will be loaded try +;; +;; mk/epm.el -q report htmlize compat +;; +;; Overview of available commands are provided by +;; +;; mk/epm.el -q help + +;;; Code: + +(require 'format-spec) +(require 'package) +(require 'subr-x) + +(defvar epm-dir nil + "Overrides `package-user-dir' and EPMDIR environment.") + +(defun epm--get-script-name () + "Guess command line argument spefifying this script. + +Real argument is not available: +`argi' is \"-scriptload\", `argval' is local variable of `command-line-1', +`load-file-name' is absolute path, `file-relative-name' is too aggressive +and adds \"..\" to root." + (let ((relative (file-relative-name load-file-name + command-line-default-directory))) + (if (string-suffix-p load-file-name relative) + load-file-name + relative))) + +(defvar epm-script-name (epm--get-script-name) + "Name of epm.el as it appears in Emacs command line options") + +(defun epm-nonempty-p (s) + (and s (not (string-empty-p s)))) + +(defun epm-init () + (unless (epm-nonempty-p epm-dir) + (setq epm-dir (getenv "EPMDIR"))) + (when (epm-nonempty-p epm-dir) + (let* ((fmt-expanded (format-spec epm-dir `((?e . ,emacs-version)))) + (dir (directory-file-name (expand-file-name fmt-expanded command-line-default-directory)))) + ;; `package-user-dir' ~/.emacs.d/elpa by default even with -Q + ;; `package-directory-list' does not include `package-user-dir'. + (setq package-user-dir dir))) + ;; TODO (load site-run-file 'no-error 'no-message) + ;; may be necessary to load elpa-* deb packages when -Q option + ;; is used. See Info node "(elisp) Init File". + (package-initialize)) + +(defun epm-library-unavailable-p (lib) + (unless (locate-library lib) + lib)) + +(defun epm-missing (libs) + ;; TODO consider `require' catching load errors + (delq nil (mapcar #'epm-library-unavailable-p libs))) + +(defun epm-cmd-help (_cmd _args) + "List commands." + (princ (format "\ +Usage: %s [--dbg|--debug-on-error] [--epm-dir DIR] COMMAND ARGS... + or: %s --script %s [--dbg|--debug-on-error] [--epm-dir DIR] COMMAND ARGS... + +A CLI tool to install ELPA packages. + +Any Emacs option may be specified, e.g. --quck,-Q, --no-init-file,-q, +or --directory,-L DIR + +--dbg, --debug-on-error + Enable `debug-on-error' + +--epm-dir DIR + Set `package-user-dir'. + \"%%e\" is replaced by `emacs-version'. + Alternatively EPMDIR environment variable may be specified. +\n" + epm-script-name (car command-line-args) epm-script-name)) + (pcase-dolist (`(,name . ,func) epm-commands) + (princ name) + (terpri) + (princ + (replace-regexp-in-string + "\\`\\|\n" "\\1 " + (documentation func) 'fixedcase nil)) + (terpri) + (terpri))) + +(defun epm-cmd-missing (_ libs) + "Report not installed libraries and exit with non-zero code." + (let ((missing (epm-missing libs))) + (when missing + (princ (mapconcat #'identity missing " ")) + (terpri) + (kill-emacs 1)))) + +(defun epm-cmd-install (_ libs) + "Install packages from LIBS that are not available yet" + ;; TODO force option or update command + (let ((missing (epm-missing libs))) + (when missing + (package-refresh-contents) + (make-directory package-user-dir 'parents)) + (dolist (pkg missing) + (package-install (intern pkg))))) + +(defun epm-cmd-report (_ libs) + "Report paths of available libraries" + (princ (format "package-user-dir: %s\n" package-user-dir)) + ;; (princ (format "load-path: %s\n" load-path)) + (dolist (name libs) + ;; (version-to-list version) + (princ (format "%-20s %s " name + (if (package-installed-p (intern name)) + "package " + " "))) + (princ (locate-library name)) + (terpri))) + +(defvar epm-commands + '(("help" . epm-cmd-help) + ("install" . epm-cmd-install) + ("missing" . epm-cmd-missing) + ("report" . epm-cmd-report))) + +(defun epm-command-line-function () + "Handle command line options and arguments specific to epm. + +Implements a handler for `command-line-functions'." + ;; There is no easy to determine if "--" argument has been processed earlier. + ;; TODO "--option=value" is handled by `command-line-1' only for standard arguments. + (pcase argi + ("--epm-dir" + (setq epm-dir (pop command-line-args-left)) + t) + ((or "--dbg" "--debug-on-error") + ;; -d is handled as --display, --debug as --debug-init + (setq debug-on-error t) + t) + ((pred (string-match-p "\\`[^-]")) + (epm-init) + (let ((func (cdr (assoc argi epm-commands))) + (cmd-args command-line-args-left)) + (if (not func) + (error "Unknown command %s" argi) + (setq command-line-args-left nil) + (funcall func argi cmd-args))) + t))) + +(push #'epm-command-line-function command-line-functions) + +;; Local Variables: +;; no-byte-compile: t +;; End: +;;; epm.el ends here -- 2.25.1