summaryrefslogtreecommitdiff
path: root/admin
diff options
context:
space:
mode:
Diffstat (limited to 'admin')
-rw-r--r--admin/scrape-elpa.el128
1 files changed, 128 insertions, 0 deletions
diff --git a/admin/scrape-elpa.el b/admin/scrape-elpa.el
new file mode 100644
index 00000000000..e1072564db6
--- /dev/null
+++ b/admin/scrape-elpa.el
@@ -0,0 +1,128 @@
+;;; scrape-elpa.el --- Collect ELPA package suggestions -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024, 2026 Free Software Foundation, Inc.
+
+;; Author: Philip Kaludercic <philipk@posteo.net>
+;; Keywords: tools
+
+;; This program 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.
+
+;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file defines an administrative command to update the
+;; `package-autosuggest' database.
+
+;;; Code:
+
+(require 'rx)
+
+(defun scrape-elpa--safe-eval (exp &optional vars)
+ "Manually evaluate EXP without potentially dangerous side-effects.
+The optional argument VARS may be an alist mapping symbols to values,
+used when evaluating variables. The evaluation function is not meant to
+be comprehensive, but just to handle the kinds of expressions that
+`scrape-elpa' expects to encounter."
+ (pcase-exhaustive exp
+ ;; special handling for macros
+ (`(rx . ,body) (rx-to-string `(: . ,body) t))
+ ;; quoting and quasi-quoting
+ (`',x x)
+ (`(purecopy ,x) x)
+ ((and (guard (eq '\` (car-safe exp))) (let `(,car . ,cdr) (cadr exp)))
+ (cons
+ (if (eq (car-safe car) '\,) (scrape-elpa--safe-eval (cadr car) vars) car)
+ (if (eq (car-safe cdr) '\,) (scrape-elpa--safe-eval (cadr cdr) vars) cdr)))
+ ;; allow calling `side-effect-free' functions
+ (`(,(and (pred symbolp) (pred (get _ 'side-effect-free)) fn) . ,args)
+ (apply fn (mapcar #'scrape-elpa--safe-eval args)))
+ ;; self-evaluating forms
+ ((pred macroexp-const-p) exp)
+ ;; variable evaluation
+ ((pred symbolp)
+ (let ((ent (assq exp vars)))
+ (if ent (cdr ent) (signal 'void-variable exp))))))
+
+(defun scrape-elpa (&rest directories)
+ "Scrape autoload files in DIRECTORIES for package suggestions.
+This file will automatically update \"package-autosuggest.eld\", but not
+save it. You should invoke this command with built GNU ELPA and NonGNU
+ELPA checkouts (i.e. having run \"make autoloads\" in both directories).
+Please review the results before updating the autosuggest database!"
+ (interactive (completing-read-multiple
+ "ELPA directories to scrape: "
+ #'completion-file-name-table
+ #'file-directory-p))
+ (with-current-buffer
+ (find-file (expand-file-name "package-autosuggest.eld" data-directory))
+ (erase-buffer)
+ (lisp-data-mode)
+ (insert ";; The contents of this file are loaded into `package--autosuggest-database'.
+;; were automatically generate by scraping ELPA for auto-loaded
+;; code using the `scrape-elpa' command from admin/scrape-elpa.el. Please do not
+;; update this file manually!
+
+")
+ (fill-paragraph)
+ (insert "(")
+ (let ((standard-output (current-buffer)))
+ (dolist-with-progress-reporter
+ (file (mapcan
+ (lambda (dir)
+ (directory-files-recursively
+ dir "-autoloads\\.el\\'"))
+ directories))
+ "Scraping files..."
+ (and-let* (((string-match "/\\([^/]+?\\)-autoloads\\.el\\'" file))
+ (pkg (intern (match-string 1 file)))
+ (vars (list '(#:nihil)))
+ (inhibit-message t))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (condition-case nil
+ (while t
+ (dolist (exp (macroexp-unprogn (read (current-buffer))))
+ (pcase exp
+ (`(defconst ,(and (pred symbolp) var) ,val . ,_)
+ (catch 'ignore
+ (push
+ (cons var (condition-case err
+ (scrape-elpa--safe-eval val vars)
+ (t (message "Failed to evaluate %S: %S in %S" exp err vars)
+ (throw 'ignore nil))))
+ vars)))
+ (`(add-to-list
+ ',(and (or 'interpreter-mode-alist
+ 'magic-mode-alist
+ 'auto-mode-alist)
+ variable)
+ ,(let `(,(and (pred stringp) regexp) .
+ ,(and (pred symbolp) mode))
+ (condition-case err
+ (scrape-elpa--safe-eval _ vars)
+ (t (message "Failed to evaluate %S: %S in %S" exp err vars)
+ nil))))
+ (terpri)
+ (prin1 (append (list pkg variable regexp)
+ (and (not (eq pkg mode)) (list mode)))))
+ (`(add-to-list
+ ',(or 'interpreter-mode-alist
+ 'magic-mode-alist
+ 'auto-mode-alist)
+ _)
+ (_ (message "Skipped over %S" exp))))))
+ (end-of-file nil))))))
+ (insert "\n)\n")))
+
+(provide 'scrape-elpa)
+;;; scrape-elpa.el ends here