emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
* [PATCH] Provide a uniform way to inform users about missing third-party packages
@ 2023-01-23 15:19 Ihor Radchenko
  0 siblings, 0 replies; only message in thread
From: Ihor Radchenko @ 2023-01-23 15:19 UTC (permalink / raw)
  To: emacs-orgmode

[-- Attachment #1: Type: text/plain, Size: 1079 bytes --]

Hi,

In Org code, we have various places where we depend (optionally) on
third-party packages. The packages are typically loaded using `require'
and report the usual error about missing library if the third-party
package is not installed.

Such situation is _almost_ OK, except some scenarios when the feature
name we in `require' is not the same with the actual package name.
Inexperienced users may easily be confused by such errors.

In the attached patch, I am introducing a new macro that can be used
instead of `require' as the following:

(org-require-package 'skewer-repl "skewer-mode")

The macro calls `require' and if it fails, displays error/warning saying

"`this-command' failed to load required package \"package name\""

It will assist users about the actual third-party package name to be
installed.

WDYT?

While writing a patch, I also noticed
`org-cite-csl--barf-without-citeproc' doing similar job.
Should we just use the new macro instead?

Also, https://github.com/bard/mozrepl ob-js backend is no longer
supported. I am wondering if we should remove it.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Provide-a-uniform-way-to-inform-users-about-missing-.patch --]
[-- Type: text/x-patch, Size: 18844 bytes --]

From 0140976d32f6fef3c2e2fa181f210dbd026008fe Mon Sep 17 00:00:00 2001
Message-Id: <0140976d32f6fef3c2e2fa181f210dbd026008fe.1674486794.git.yantar92@posteo.net>
From: Ihor Radchenko <yantar92@posteo.net>
Date: Mon, 23 Jan 2023 18:06:46 +0300
Subject: [PATCH] Provide a uniform way to inform users about missing
 third-party packages

* lisp/org-macs.el (org-require-package): New macro trying to load a
library and displaying custom error message or warning on failure.
The actual package (not library) name can be provided as optional
argument.

* lisp/ob-R.el (org-babel-R-initiate-session):
* lisp/ob-clojure.el (ob-clojure-eval-with-inf-clojure):
(ob-clojure-eval-with-cider):
(ob-clojure-eval-with-slime):
* lisp/ob-forth.el (org-babel-forth-session-execute):
* lisp/ob-gnuplot.el (org-babel-execute:gnuplot):
(org-babel-gnuplot-initiate-session):
* lisp/ob-haskell.el (org-babel-interpret-haskell):
(org-babel-haskell-initiate-session):
* lisp/ob-js.el (org-babel-execute:js):
(org-babel-js-initiate-session):
* lisp/ob-julia.el (org-babel-julia-initiate-session):
* lisp/ob-lisp.el (org-babel-execute:lisp):
* lisp/ob-ocaml.el (org-babel-prep-session:ocaml):
* lisp/ob-octave.el (org-babel-octave-initiate-session):
* lisp/ob-processing.el (org-babel-processing-view-sketch):
* lisp/ob-ruby.el (org-babel-execute:ruby):
(org-babel-ruby-initiate-session):
* lisp/ol-bbdb.el (org-bbdb-open):
(org-bbdb-complete-link):
(org-bbdb-anniv-export-ical):
* lisp/org-agenda.el:
* lisp/org-plot.el (org-plot/gnuplot):
* lisp/org.el:
* lisp/ox-ascii.el (org-ascii-table):
* lisp/ox-html.el (org-html-htmlize-generate-css):
* lisp/ox-org.el: Use the new macro.

* lisp/oc-csl.el: Add FIXME. oc-csl uses a custom function doing
similar job.

* lisp/ob-js.el (org-babel-js-initiate-session): Add FIXME noting that
the third-party package is outdated.
---
 lisp/ob-R.el          |  2 +-
 lisp/ob-clojure.el    |  9 +++------
 lisp/ob-forth.el      |  2 +-
 lisp/ob-gnuplot.el    |  4 ++--
 lisp/ob-haskell.el    |  4 ++--
 lisp/ob-js.el         | 10 ++++++----
 lisp/ob-julia.el      |  3 ++-
 lisp/ob-lisp.el       |  6 +++---
 lisp/ob-ocaml.el      |  2 +-
 lisp/ob-octave.el     |  6 ++++--
 lisp/ob-processing.el |  2 +-
 lisp/ob-ruby.el       |  4 ++--
 lisp/oc-csl.el        |  1 +
 lisp/ol-bbdb.el       |  8 ++++----
 lisp/org-agenda.el    |  3 +--
 lisp/org-macs.el      |  9 +++++++++
 lisp/org-plot.el      |  2 +-
 lisp/org.el           |  2 +-
 lisp/ox-ascii.el      |  2 +-
 lisp/ox-html.el       |  3 +--
 lisp/ox-org.el        |  3 +--
 21 files changed, 48 insertions(+), 39 deletions(-)

diff --git a/lisp/ob-R.el b/lisp/ob-R.el
index 4ee091118..2d22a4657 100644
--- a/lisp/ob-R.el
+++ b/lisp/ob-R.el
@@ -276,7 +276,7 @@ (defun org-babel-R-initiate-session (session params)
 	  (when (get-buffer session)
 	    ;; Session buffer exists, but with dead process
 	    (set-buffer session))
-	  (require 'ess) (R)
+          (org-require-package 'ess "ESS") (R)
 	  (let ((R-proc (get-process (or ess-local-process-name
 					 ess-current-process-name))))
 	    (while (process-get R-proc 'callbacks)
diff --git a/lisp/ob-clojure.el b/lisp/ob-clojure.el
index d993e0cb7..5f589db00 100644
--- a/lisp/ob-clojure.el
+++ b/lisp/ob-clojure.el
@@ -186,8 +186,7 @@ (defvar comint-prompt-regexp)
 (defvar inf-clojure-comint-prompt-regexp)
 (defun ob-clojure-eval-with-inf-clojure (expanded params)
   "Evaluate EXPANDED code block with PARAMS using inf-clojure."
-  (condition-case nil (require 'inf-clojure)
-    (user-error "inf-clojure not available"))
+  (org-require-package 'inf-clojure)
   ;; Maybe initiate the inf-clojure session
   (unless (and inf-clojure-buffer
 	       (buffer-live-p (get-buffer inf-clojure-buffer)))
@@ -228,8 +227,7 @@ (defun ob-clojure-eval-with-inf-clojure (expanded params)
 
 (defun ob-clojure-eval-with-cider (expanded params)
   "Evaluate EXPANDED code block with PARAMS using cider."
-  (condition-case nil (require 'cider)
-    (user-error "cider not available"))
+  (org-require-package 'cider "Cider")
   (let ((connection (cider-current-connection (cdr (assq :target params))))
 	(result-params (cdr (assq :result-params params)))
 	result0)
@@ -256,8 +254,7 @@ (defun ob-clojure-eval-with-cider (expanded params)
 
 (defun ob-clojure-eval-with-slime (expanded params)
   "Evaluate EXPANDED code block with PARAMS using slime."
-  (condition-case nil (require 'slime)
-    (user-error "slime not available"))
+  (org-require-package 'slime "SLIME")
   (with-temp-buffer
     (insert expanded)
     (slime-eval
diff --git a/lisp/ob-forth.el b/lisp/ob-forth.el
index e5dcad6d0..bd9ec04a4 100644
--- a/lisp/ob-forth.el
+++ b/lisp/ob-forth.el
@@ -55,7 +55,7 @@ (defun org-babel-execute:forth (body params)
 	(car (last all-results))))))
 
 (defun org-babel-forth-session-execute (body params)
-  (require 'forth-mode)
+  (org-require-package 'forth-mode)
   (let ((proc (forth-proc))
 	(rx " \\(\n:\\|compiled\n\\|ok\n\\)")
 	(result-start))
diff --git a/lisp/ob-gnuplot.el b/lisp/ob-gnuplot.el
index e3e42918c..79770c7fd 100644
--- a/lisp/ob-gnuplot.el
+++ b/lisp/ob-gnuplot.el
@@ -198,7 +198,7 @@ (defun org-babel-expand-body:gnuplot (body params)
 (defun org-babel-execute:gnuplot (body params)
   "Execute a block of Gnuplot code.
 This function is called by `org-babel-execute-src-block'."
-  (require 'gnuplot)
+  (org-require-package 'gnuplot)
   (let ((session (cdr (assq :session params)))
         (result-type (cdr (assq :results params)))
         (body (org-babel-expand-body:gnuplot body params))
@@ -262,7 +262,7 @@ (defun org-babel-gnuplot-initiate-session (&optional session _params)
 If there is not a current inferior-process-buffer in SESSION
 then create one.  Return the initialized session.  The current
 `gnuplot-mode' doesn't provide support for multiple sessions."
-  (require 'gnuplot)
+  (org-require-package 'gnuplot)
   (unless (string= session "none")
     (save-window-excursion
       (gnuplot-send-string-to-gnuplot "" "line")
diff --git a/lisp/ob-haskell.el b/lisp/ob-haskell.el
index 7185ed61f..2b1441c2a 100644
--- a/lisp/ob-haskell.el
+++ b/lisp/ob-haskell.el
@@ -122,7 +122,7 @@ (defun org-babel-haskell-execute (body params)
 	  (cdr (assq :rowname-names params)) (cdr (assq :rownames params))))))))
 
 (defun org-babel-interpret-haskell (body params)
-  (require 'inf-haskell)
+  (org-require-package 'inf-haskell "haskell-mode")
   (add-hook 'inferior-haskell-hook
             (lambda ()
               (setq-local comint-prompt-regexp
@@ -167,7 +167,7 @@ (defun org-babel-haskell-initiate-session (&optional _session _params)
   "Initiate a haskell session.
 If there is not a current inferior-process-buffer in SESSION
 then create one.  Return the initialized session."
-  (require 'inf-haskell)
+  (org-require-package 'inf-haskell "haskell-mode")
   (or (get-buffer "*haskell*")
       (save-window-excursion (run-haskell) (sleep-for 0.25) (current-buffer))))
 
diff --git a/lisp/ob-js.el b/lisp/ob-js.el
index 910c11686..e19a6e543 100644
--- a/lisp/ob-js.el
+++ b/lisp/ob-js.el
@@ -96,7 +96,7 @@ (defun org-babel-execute:js (body params)
 		  ;; Indium Node REPL.  Separate case because Indium
 		  ;; REPL is not inherited from Comint mode.
 		  ((string= session "*JS REPL*")
-		   (require 'indium-repl)
+                   (org-require-package 'indium-repl "indium")
 		   (unless (get-buffer session)
 		     (indium-run-node org-babel-js-cmd))
 		   (indium-eval full-body))
@@ -168,7 +168,7 @@ (defun org-babel-js-initiate-session (&optional session _params)
    ((string= session "none")
     (warn "Session evaluation of ob-js is not supported"))
    ((string= "*skewer-repl*" session)
-    (require 'skewer-repl)
+    (org-require-package 'skewer-repl "skewer-mode")
     (let ((session-buffer (get-buffer "*skewer-repl*")))
       (if (and session-buffer
 	       (org-babel-comint-buffer-livep (get-buffer session-buffer))
@@ -180,7 +180,7 @@ (defun org-babel-js-initiate-session (&optional session _params)
 	(skewer-repl)
 	session-buffer)))
    ((string= "*Javascript REPL*" session)
-    (require 'js-comint)
+    (org-require-package 'js-comint)
     (let ((session-buffer "*Javascript REPL*"))
       (if (and (org-babel-comint-buffer-livep (get-buffer session-buffer))
 	       (comint-check-proc session-buffer))
@@ -189,7 +189,9 @@ (defun org-babel-js-initiate-session (&optional session _params)
 	(sit-for .5)
 	session-buffer)))
    ((string= "mozrepl" org-babel-js-cmd)
-    (require 'moz)
+    ;; FIXME: According to https://github.com/bard/mozrepl, this REPL
+    ;; is outdated and does not work for Firefox >54.
+    (org-require-package 'moz "mozrepl")
     (let ((session-buffer (save-window-excursion
 			    (run-mozilla nil)
 			    (rename-buffer session)
diff --git a/lisp/ob-julia.el b/lisp/ob-julia.el
index cb5c7fa3b..495169da1 100644
--- a/lisp/ob-julia.el
+++ b/lisp/ob-julia.el
@@ -196,7 +196,8 @@ (defun org-babel-julia-initiate-session (session params)
 	  (when (get-buffer session)
 	    ;; Session buffer exists, but with dead process
 	    (set-buffer session))
-          (require 'ess) (set-buffer (julia))
+          (org-require-package 'ess "ESS")
+          (set-buffer (julia))
 	  (rename-buffer
 	   (if (bufferp session)
 	       (buffer-name session)
diff --git a/lisp/ob-lisp.el b/lisp/ob-lisp.el
index 048ef883c..03f23c82d 100644
--- a/lisp/ob-lisp.el
+++ b/lisp/ob-lisp.el
@@ -90,9 +90,9 @@ (defun org-babel-execute:lisp (body params)
   "Execute a block of Common Lisp code with Babel.
 BODY is the contents of the block, as a string.  PARAMS is
 a property list containing the parameters of the block."
-  (require (pcase org-babel-lisp-eval-fn
-	     (`slime-eval 'slime)
-	     (`sly-eval 'sly)))
+  (pcase org-babel-lisp-eval-fn
+    (`slime-eval (org-require-package 'slime "SLIME"))
+    (`sly-eval (org-require-package 'sly "SLY")))
   (org-babel-reassemble-table
    (let ((result
           (funcall (if (member "output" (cdr (assq :result-params params)))
diff --git a/lisp/ob-ocaml.el b/lisp/ob-ocaml.el
index 09224b98b..4ab58e150 100644
--- a/lisp/ob-ocaml.el
+++ b/lisp/ob-ocaml.el
@@ -109,7 +109,7 @@ (defun org-babel-execute:ocaml (body params)
 (defvar tuareg-interactive-buffer-name)
 (defun org-babel-prep-session:ocaml (session _params)
   "Prepare SESSION according to the header arguments in PARAMS."
-  (require 'tuareg)
+  (org-require-package 'tuareg)
   (let ((tuareg-interactive-buffer-name (if (and (not (string= session "none"))
                                                  (not (string= session "default"))
                                                  (stringp session))
diff --git a/lisp/ob-octave.el b/lisp/ob-octave.el
index 9bf16b984..64eb2a32a 100644
--- a/lisp/ob-octave.el
+++ b/lisp/ob-octave.el
@@ -154,8 +154,10 @@ (defun org-babel-octave-initiate-session (&optional session _params matlabp)
   "Create an octave inferior process buffer.
 If there is not a current inferior-process-buffer in SESSION then
 create.  Return the initialized session."
-  (if matlabp (require 'matlab) (or (require 'octave-inf nil 'noerror)
-				    (require 'octave)))
+  (if matlabp
+      (org-require-package 'matlab "matlab-mode")
+    (or (require 'octave-inf nil 'noerror)
+	(require 'octave)))
   (unless (string= session "none")
     (let ((session (or session
 		       (if matlabp "*Inferior Matlab*" "*Inferior Octave*"))))
diff --git a/lisp/ob-processing.el b/lisp/ob-processing.el
index 4eeaf98e0..460e6e381 100644
--- a/lisp/ob-processing.el
+++ b/lisp/ob-processing.el
@@ -78,7 +78,7 @@ (defvar org-babel-processing-processing-js-filename "processing.js"
 (defun org-babel-processing-view-sketch ()
   "Show the sketch of the Processing block under point in an external viewer."
   (interactive)
-  (require 'processing-mode)
+  (org-require-package 'processing-mode)
   (let ((info (org-babel-get-src-block-info)))
     (if (string= (nth 0 info) "processing")
 	(let* ((body (nth 1 info))
diff --git a/lisp/ob-ruby.el b/lisp/ob-ruby.el
index b94bc73dd..ba8697731 100644
--- a/lisp/ob-ruby.el
+++ b/lisp/ob-ruby.el
@@ -86,7 +86,7 @@ (defun org-babel-execute:ruby (body params)
 		     body params (org-babel-variable-assignments:ruby params)))
          (result (if (member "xmp" result-params)
 		     (with-temp-buffer
-		       (require 'rcodetools)
+		       (org-require-package 'rcodetools "rcodetools (gem package)")
 		       (insert full-body)
 		       (xmp (cdr (assq :xmp-option params)))
 		       (buffer-string))
@@ -161,7 +161,7 @@ (defun org-babel-ruby-initiate-session (&optional session params)
 If there is not a current inferior-process-buffer in SESSION
 then create one.  Return the initialized session."
   (unless (string= session "none")
-    (require 'inf-ruby)
+    (org-require-package 'inf-ruby)
     (let* ((command (cdr (or (assq :ruby params)
 			     (assoc inf-ruby-default-implementation
 				    inf-ruby-implementations))))
diff --git a/lisp/oc-csl.el b/lisp/oc-csl.el
index 432738a97..94c2ed94c 100644
--- a/lisp/oc-csl.el
+++ b/lisp/oc-csl.el
@@ -417,6 +417,7 @@ (defconst org-cite-csl--label-regexp
 
 \f
 ;;; Internal functions
+;; FIXME: We use `org-require-package' in other places.
 (defun org-cite-csl--barf-without-citeproc ()
   "Raise an error if Citeproc library is not loaded."
   (unless (featurep 'citeproc)
diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el
index 47bd9d98c..915d83df2 100644
--- a/lisp/ol-bbdb.el
+++ b/lisp/ol-bbdb.el
@@ -255,7 +255,7 @@ (defun org-bbdb-export (path desc format _)
 
 (defun org-bbdb-open (name _)
   "Follow a BBDB link to NAME."
-  (require 'bbdb-com)
+  (org-require-package 'bbdb-com "bbdb")
   (let ((inhibit-redisplay (not debug-on-error)))
     (if (fboundp 'bbdb-name)
 	(org-bbdb-open-old name)
@@ -369,7 +369,7 @@ (defun org-bbdb-anniversaries ()
   "Extract anniversaries from BBDB for display in the agenda.
 When called programmatically, this function expects the `date'
 variable to be globally bound."
-  (require 'bbdb)
+  (org-require-package 'bbdb)
   (require 'diary-lib)
   (unless (hash-table-p org-bbdb-anniv-hash)
     (setq org-bbdb-anniv-hash
@@ -500,7 +500,7 @@ (defun org-bbdb-anniversaries-future (&optional n)
 
 (defun org-bbdb-complete-link ()
   "Read a bbdb link with name completion."
-  (require 'bbdb-com)
+  (org-require-package 'bbdb-com "bbdb")
   (let ((rec (bbdb-completing-read-record "Name: ")))
     (concat "bbdb:"
 	    (bbdb-record-name (if (listp rec)
@@ -509,7 +509,7 @@ (defun org-bbdb-complete-link ()
 
 (defun org-bbdb-anniv-export-ical ()
   "Extract anniversaries from BBDB and convert them to icalendar format."
-  (require 'bbdb)
+  (org-require-package 'bbdb)
   (require 'diary-lib)
   (unless (hash-table-p org-bbdb-anniv-hash)
     (setq org-bbdb-anniv-hash
diff --git a/lisp/org-agenda.el b/lisp/org-agenda.el
index d2aa8f3f2..3f2b5dcc8 100644
--- a/lisp/org-agenda.el
+++ b/lisp/org-agenda.el
@@ -3627,8 +3627,7 @@ (defun org-agenda-write (file &optional open nosettings agenda-bufname)
 		(kill-buffer (current-buffer))
 		(message "Org file written to %s" file)))
 	     ((member extension '("html" "htm"))
-	      (or (require 'htmlize nil t)
-		  (error "Please install htmlize from https://github.com/hniksic/emacs-htmlize"))
+              (org-require-package 'htmlize)
 	      (declare-function htmlize-buffer "htmlize" (&optional buffer))
 	      (set-buffer (htmlize-buffer (current-buffer)))
 	      (when org-agenda-export-html-style
diff --git a/lisp/org-macs.el b/lisp/org-macs.el
index cda9c5e03..9083b82f8 100644
--- a/lisp/org-macs.el
+++ b/lisp/org-macs.el
@@ -107,6 +107,15 @@ (defvar org-fold-core-style)
 \f
 ;;; Macros
 
+(defmacro org-require-package (symbol &optional name noerror)
+  "Try to load library SYMBOL and display error otherwise.
+With optional parameter NAME, use NAME as package name instead of
+SYMBOL.  Show warning instead of error when NOERROR is non-nil."
+  `(unless (require ,symbol nil t)
+     (,(if noerror 'warn 'user-error)
+      "`%s' failed to load required package \"%s\""
+      this-command ,(or name symbol))))
+
 (defmacro org-with-gensyms (symbols &rest body)
   (declare (debug (sexp body)) (indent 1))
   `(let ,(mapcar (lambda (s)
diff --git a/lisp/org-plot.el b/lisp/org-plot.el
index fe61e9ace..d39ffc4b4 100644
--- a/lisp/org-plot.el
+++ b/lisp/org-plot.el
@@ -641,7 +641,7 @@ (defun org-plot/gnuplot (&optional params)
 If not given options will be taken from the +PLOT
 line directly before or after the table."
   (interactive)
-  (require 'gnuplot)
+  (org-require-package 'gnuplot)
   (save-window-excursion
     (delete-other-windows)
     (when (get-buffer "*gnuplot*") ; reset *gnuplot* if it already running
diff --git a/lisp/org.el b/lisp/org.el
index 00674d1fc..958305382 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -15436,7 +15436,7 @@ (define-minor-mode org-cdlatex-mode
 \\{org-cdlatex-mode-map}"
   :lighter " OCDL"
   (when org-cdlatex-mode
-    (require 'cdlatex)
+    (org-require-package 'cdlatex)
     (run-hooks 'cdlatex-mode-hook)
     (cdlatex-compute-tables))
   (unless org-cdlatex-texmathp-advice-is-done
diff --git a/lisp/ox-ascii.el b/lisp/ox-ascii.el
index 9c4424b14..e5bdf92cb 100644
--- a/lisp/ox-ascii.el
+++ b/lisp/ox-ascii.el
@@ -1856,7 +1856,7 @@ (defun org-ascii-table (table contents info)
       (cond ((eq (org-element-property :type table) 'org) contents)
 	    ((and (plist-get info :ascii-table-use-ascii-art)
 		  (eq (plist-get info :ascii-charset) 'utf-8)
-		  (require 'ascii-art-to-unicode nil t))
+		  (org-require-package 'ascii-art-to-unicode nil 'noerror))
 	     (with-temp-buffer
 	       (insert (org-remove-indentation
 			(org-element-property :value table)))
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 5e58ccba3..37c474409 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -1823,8 +1823,7 @@ (defun org-html-htmlize-generate-css ()
 to the function `org-html-htmlize-region-for-paste' will
 produce code that uses these same face definitions."
   (interactive)
-  (unless (require 'htmlize nil t)
-    (error "htmlize library missing.  Aborting"))
+  (org-require-package 'htmlize)
   (and (get-buffer "*html*") (kill-buffer "*html*"))
   (with-temp-buffer
     (let ((fl (face-list))
diff --git a/lisp/ox-org.el b/lisp/ox-org.el
index ed72cf4f2..9329eb159 100644
--- a/lisp/ox-org.el
+++ b/lisp/ox-org.el
@@ -320,8 +320,7 @@ (defun org-org-publish-to-org (plist filename pub-dir)
 Return output file name."
   (org-publish-org-to 'org filename ".org" plist pub-dir)
   (when (plist-get plist :htmlized-source)
-    (or (require 'htmlize nil t)
-	(error "Please install htmlize from https://github.com/hniksic/emacs-htmlize"))
+    (org-require-package 'htmlize)
     (require 'ox-html)
     (let* ((org-inhibit-startup t)
 	   (htmlize-output-type 'css)
-- 
2.39.1


[-- Attachment #3: Type: text/plain, Size: 224 bytes --]


-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2023-01-23 15:20 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-01-23 15:19 [PATCH] Provide a uniform way to inform users about missing third-party packages Ihor Radchenko

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).