diff options
| author | Craig Jennings <c@cjennings.net> | 2024-04-07 13:41:34 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2024-04-07 13:41:34 -0500 |
| commit | 754bbf7a25a8dda49b5d08ef0d0443bbf5af0e36 (patch) | |
| tree | f1190704f78f04a2b0b4c977d20fe96a828377f1 /custom/elpa-mirror.el | |
new repository
Diffstat (limited to 'custom/elpa-mirror.el')
| -rw-r--r-- | custom/elpa-mirror.el | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/custom/elpa-mirror.el b/custom/elpa-mirror.el new file mode 100644 index 00000000..777293e2 --- /dev/null +++ b/custom/elpa-mirror.el @@ -0,0 +1,450 @@ +;;; elpa-mirror.el --- Create local package repository from installed packages + +;; Copyright (C) 2014-2020 Chen Bin + +;; Author: Chen Bin <chenbin.sh@gmail.com> +;; URL: http://github.com/redguardtoo/elpa-mirror +;; Package-Requires: ((emacs "25.1")) +;; Version: 2.2.2 +;; Keywords: tools +;; +;; This file is not part of GNU Emacs. + +;;; License: + +;; This file is part of elpa-mirror +;; +;; elpa-mirror 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. +;; +;; elpa-mirror 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This program will create a local package repository by from all +;; installed packages. +;; +;; Please note compile Emacs Lisp file (*.elc) from one version of Emacs +;; might not work with another version of Emacs. So you need this program +;; to compile package from local repository. +;; +;; This is the ONLY way to have 100% portable Emacs setup. +;; +;; Usage in Emacs, +;; Run `elpamr-create-mirror-for-installed'. +;; +;; CLI program tar is required. It's bundled with Windows10/Linux/macOS. +;; +;; Usage in Shell, +;; Emacs --batch -l ~/.emacs.d/init.el +;; -l ~/any-directory-you-prefer/elpa-mirror.el \ +;; --eval='(setq elpamr-default-output-directory "~/myelpa")' \ +;; --eval='(elpamr-create-mirror-for-installed) +;; +;; Use the repository created by elpa-mirror, +;; - Add `(setq package-archives '(("myelpa" . "~/myelpa/")))` into ~/.emacs +;; - Restart Emacs +;; +;; Tips, +;; - `elpamr-exclude-packages' excludes packages +;; +;; - `elpamr-tar-command-exclude-patterns' excludes file and directories in +;; package directory. +;; +;; - `elpamr-exclude-patterns-filter-function' lets users define a function to +;; exclude files and directories per package. +;; +;; Below setup adds directory "bin/" into package "vagrant-tramp". +;; +;; (setq elpamr-exclude-patterns-filter-function +;; (lambda (package-dir) +;; (let ((patterns elpamr-tar-command-exclude-patterns)) +;; (when (string-match "vagrant-tramp" package-dir) +;; (setq patterns (remove "*/bin" patterns))) +;; patterns))) + +;; - You can also setup repositories on Dropbox and Github. +;; See https://github.com/redguardtoo/elpa-mirror for details. +;; + +;;; Code: +(require 'package) + +(defcustom elpamr-default-output-directory nil + "The output directory use by `elpamr-create-mirror-for-installed'." + :type '(choice directory (const :tags "None" nil)) + :group 'elpa-mirror) + +(defcustom elpamr-exclude-packages nil + "Names of excluded packages." + :type '(repeat string) + :group 'elpa-mirror) + +(defcustom elpamr-tar-command-exclude-patterns + '("*.elc" + "*~" + "*autoloads.el" + "*.so" + "*.dylib" + "*.dll" + "*/bin" + "*/__pycache__") + "Exclude patterns passed tar's `--exclude' option. + +The patterns use shell glob syntax, not regexp syntax: + +* `*' matches any string, including `/'. +* `?' matches a single character. +* `[abc]' or `[a-z]' is a character class. +* `[^a-z]' or `[!a-z]' is a negated character class. +* `^' and `$' have a special meaning in BSD tar only. +* Special characters are quoted with `\\'. + +The patterns are anchored, meaning that they always start +matching at the start of the path. This is done by passing the +`--anchored' option when running with GNU tar, or pre-pending `^' +to every pattern when running with BSD tar. + +Examples: + +* Exclude files/directories that end with `.elc': `*.elc'. +* Exclude files/directories named `__pycache__': `*/__pycache__'. +* Exclude `bin' inside the `company' package: `company-*/bin'. + +Note that a slash at the start or the end of a pattern will cause +it to match nothing." + :type '(repeat string) + :group 'elpa-mirror) + +(defcustom elpamr-exclude-patterns-filter-function nil + "Filter `elpamr-tar-command-exclude-patterns' before using it per package. +A function with one parameter which is the package directory. +It returns the result to replace `elpamr-tar-command-exclude-patterns'." + :group 'elpa-mirror + :type 'hook) + +(defcustom elpamr-tar-executable + "tar" + "The tar executable used by elpa-mirror. +It can be BSD tar, but GNU tar is preferred." + :type 'string + :group 'elpa-mirror) + +(defcustom elpamr-finished-hook nil + "Hook run when command `elpamr-create-mirror-for-installed' run finished. +The hook function have one argument: output-directory." + :group 'elpa-mirror + :type 'hook) + +(defcustom elpamr-enable-log nil + "Enable log." + :type 'boolean + :group 'elpa-mirror) + +(defvar elpamr--log-buffer "*elpa-mirror log*" + "Destination buffer for log messages and command output.") + +(defun elpamr--log (format-string &rest args) + "Format ARGS with FORMAT-STRING, add the result to the log, and return it. +The log line will be pre-pended with an asterisk to distinguish it +from program output." + (when elpamr-enable-log + (let ((line (apply #'format format-string args))) + (with-current-buffer (get-buffer-create elpamr--log-buffer) + (insert "* " line "\n")) + line))) + +(defun elpamr--log-message (format-string &rest args) + "Format ARGS with FORMAT-STRING, add the result to the log and display it." + (when elpamr-enable-log + (apply #'elpamr--log format-string args) + (apply #'message format-string args))) + +(defun elpamr--log-error (format-string &rest args) + "Format ARGS with FORMAT-STRING, add the result to the log and signal an error." + (when elpamr-enable-log +(apply #'elpamr--log format-string args) + (apply #'error format-string args))) + +(defun elpamr--package-desc (item) + "Extract package information from ITEM." + (cadr item)) + +(defun elpamr--is-bsd-tar () + "Are we using BSD tar instead of GNU tar?" + (let* ((output (mapconcat #'identity (process-lines elpamr-tar-executable "--version") " ")) + ;; @see https://github.com/redguardtoo/elpa-mirror/issues/37 + ;; extra error message insert extra whitespace before "bsdtar" + (result (and output (string-match-p "\\(^[ \t]*\\|[ \t]\\)bsdtar" output)))) + (elpamr--log "Detected tar variant: %s" (if result "BSD" "GNU")) + result)) + +(defun elpamr--create-one-item-for-archive-contents (pkg) + "Access PKG extracted from `package-alist' directly." + (unless (member (symbol-name (car pkg)) elpamr-exclude-packages) + pkg)) + +(defun elpamr--fullpath (parent file) + "Full path of 'PARENT/FILE'." + (let* ((result (file-truename (concat (file-name-as-directory parent) file)))) + (elpamr--log "Converted to full path: %S %S -> %S" parent file result) + result)) + +(defun elpamr--clean-package-description (description) + "Clean DESCRIPTION." + (replace-regexp-in-string "-\*-.*-\*-" "" + (replace-regexp-in-string "\"" "" description t) + t)) + +(defun elpamr--get-dependency (item) + "Get ITEM dependency." + (package-desc-reqs (elpamr--package-desc item))) + +(defun elpamr--get-version (item) + "Get ITEM version." + (package-desc-version (elpamr--package-desc item))) + +(defun elpamr--get-summary (item) + "Get ITEM description." + (package-desc-summary (elpamr--package-desc item))) + +(defun elpamr--one-item-for-archive-contents (final-pkg) + "Format FINAL-PKG information into a string for archive-contents." + (format " (%s . [%S %S \"%s\" tar])\n" + (car final-pkg) + (elpamr--get-version final-pkg) + (elpamr--get-dependency final-pkg) + (elpamr--clean-package-description (elpamr--get-summary final-pkg)))) + +(defun elpamr--call-process-check (arguments) + "Call run tar program with the ARGUMENTS. +Log and signal an error if it exits with a non-zero status." + (let ((exit-status (apply #'call-process + elpamr-tar-executable + nil + (and elpamr-enable-log elpamr--log-buffer) + nil + arguments))) + (cond + ((not (= exit-status 0)) + (elpamr--log-error + "Program %s exited with non-zero status %s, see the %s buffer for details" + elpamr-tar-executable exit-status elpamr--log-buffer) + ) + (t + exit-status)))) + +(defun elpamr--run-tar (working-dir out-file dir-to-archive is-bsd-tar) + "Run tar in order to archive DIR-TO-ARCHIVE into OUT-FILE. +Paths are relative to WORKING-DIR. +IS-BSD-TAR should be non-nil if this function should use a +command compatible with BSD tar instead of GNU tar." + ;; We could detect BSD tar inside this function easily, but detecting it once + ;; and then passing it as an argument improves performance. + (let* ((exclude-opts (mapcar (lambda (s) + (concat "--exclude=" (if is-bsd-tar "^" "") s)) + (if elpamr-exclude-patterns-filter-function + (funcall elpamr-exclude-patterns-filter-function + dir-to-archive) + elpamr-tar-command-exclude-patterns))) + ;; set pwd of process + (default-directory working-dir) + ;; create tar using GNU tar + (tar-args + `("cf" ,out-file + ,@(unless is-bsd-tar '("--anchored")) + ,@exclude-opts + ;; tar 1.14 NEWS, + ;; @see https://git.savannah.gnu.org/cgit/tar.git/plain/NEWS?id=release_1_14 + ;; * New option --format allows to select the output archive format + ;; * The default output format can be selected at configuration time + ;; by presetting the environment variable DEFAULT_ARCHIVE_FORMAT. + ;; Allowed values are GNU, V7, OLDGNU and POSIX. + ,@(unless is-bsd-tar '("--format=gnu")) + ;; Improve reproducibility by not storing unnecessary metadata. + ;; These options are enough for archives in the GNU format, but if + ;; we ever switch to PAX, we'll need to add more (see + ;; <http://h2.jaguarpaw.co.uk/posts/reproducible-tar/> and + ;; <https://www.gnu.org/software/tar/manual/html_node/PAX-keywords.html>). + ,@(unless is-bsd-tar + ;; @see https://github.com/redguardtoo/elpa-mirror/issues/41 + '("--owner=root" + "--group=root" + "--mtime=1970-01-01 00:00:00 UTC")) + "--" ,dir-to-archive)) + ;; Don't archive macOS' file properties (see + ;; <https://superuser.com/q/259703>). + (process-environment (if (eq system-type 'darwin) + (cons "COPYFILE_DISABLE=" process-environment) + process-environment))) + (elpamr--log "Running tar: %S %S" elpamr-tar-executable tar-args) + (elpamr--call-process-check tar-args))) + +;;;###autoload +(defun elpamr-version () + "Current version." + (interactive) + (message "2.2.2")) + +(defun elpamr--win-executable-find (exe) + "Find EXE on windows." + (let* ((drivers '("c" "d" "e" "f")) + (i 0) + j + (dirs '(":\\\\cygwin64\\\\bin\\\\" + ":\\\\msys64\\\\usr\\\\bin\\\\")) + rlt) + (while (and (not rlt) + (< i (length dirs))) + (setq j 0) + (while (and (not rlt) + (< j (length drivers))) + (setq rlt (executable-find (concat (nth j drivers) (nth i dirs) exe))) + (setq j (1+ j))) + (setq i (1+ i))) + (unless rlt + ;; nothing found, fall back to exe + (setq rlt exe)) + rlt)) + +(defun elpamr-double-check-executable () + "Make sure `elpamr-tar-executable' is executable." + (when (eq system-type 'windows-nt) + + (cond + (elpamr-tar-executable + (unless (executable-find elpamr-tar-executable) + (setq elpamr-tar-executable (elpamr--win-executable-find elpamr-tar-executable)))) + + ((executable-find "tar") + (setq elpamr-tar-executable "tar"))))) + +(defun elpamr-delete-directory (directory) + "Delete DIRECTORY." + (ignore-errors + (delete-directory directory t))) + +;;;###autoload +(defun elpamr-create-mirror-for-installed (&optional output-directory recreate-directory) + "Export installed packages into a new directory. +Create the html files for the mirror site. + +The first valid directory found from the below list +will be used as mirror package's output directory: +1. Argument: OUTPUT-DIRECTORY +2. Variable: `elpamr-default-output-directory' +3. Ask user to provide. + +When RECREATE-DIRECTORY is non-nil, OUTPUT-DIRECTORY +will be deleted and recreated." + (interactive) + + ;; find tar program on Windows if GNU tar from Cygwin/MYSYS2 is installed + ;; and current `elpamr-tar-executable' is NOT executable. + (elpamr-double-check-executable) + + (let (final-pkg-list) + + ;; Erase the log (in case of multiple consecutive calls to this function). + (when elpamr-enable-log + (with-current-buffer (get-buffer-create elpamr--log-buffer) + (erase-buffer))) + + ;; Since Emacs 27, `package-initialize' is optional. + ;; but we still need it to initialize `package-alist'. + (unless package-alist (package-initialize)) + + ;; Quote from manual about package-alist: + ;; Alist of all packages available for activation. + ;; Each element has the form (PKG . DESCS), where PKG is a package + ;; name (a symbol) and DESCS is a non-empty list of `package-desc' structure, + ;; sorted by decreasing versions. + ;; Sorted for reproducibility. + (setq final-pkg-list + (let ((sorted-package-alist + (sort (copy-sequence package-alist) + (lambda (a b) + (string< (symbol-name (car a)) + (symbol-name (car b))))))) + (delq nil (mapcar #'elpamr--create-one-item-for-archive-contents + sorted-package-alist)))) + + ;; set output directory + (setq output-directory + (cond ((and output-directory + (stringp output-directory)) + (file-name-as-directory output-directory)) + ((and elpamr-default-output-directory + (stringp elpamr-default-output-directory)) + (file-name-as-directory elpamr-default-output-directory)) + (t (read-directory-name "Output directory: ")))) + + ;; Delete output directory if we need a clean output directory + (when (and recreate-directory + (file-directory-p output-directory)) + (elpamr--log-message "Re-creating %s" output-directory) + (elpamr-delete-directory output-directory)) + + ;; Create output directory if it does not exist. + (unless (file-directory-p output-directory) + (make-directory output-directory t)) + + (when (and (> (length final-pkg-list) 0) + output-directory + (file-directory-p output-directory)) + (let ((tmp-dir (make-temp-file "elpa" t)) + (is-bsd-tar (elpamr--is-bsd-tar)) + (cnt 0)) + + (dolist (package final-pkg-list) + (let* ((pkg-dir (package-desc-dir (car (cdr package)))) + (name (package-desc-name (car (cdr package)))) + (version-str (package-version-join (package-desc-version (car (cdr package))))) + (name-fixed (format "%s-%s" name version-str)) + (package-parent-dir-temp-p (not (string-match (concat "^" (file-truename package-user-dir)) + pkg-dir))) + (package-parent-dir (cond + (package-parent-dir-temp-p + tmp-dir) + (t + package-user-dir)))) + + (when package-parent-dir-temp-p + (copy-directory pkg-dir (elpamr--fullpath tmp-dir name-fixed))) + + (elpamr--run-tar package-parent-dir + (file-relative-name (concat (elpamr--fullpath output-directory name-fixed) ".tar") + package-parent-dir) + name-fixed + is-bsd-tar) + (message "Creating *.tar... %2d%% (%s)" + (/ (* cnt 100) (length final-pkg-list)) + pkg-dir)) + (setq cnt (1+ cnt))) + ;; clean up temp directory + (elpamr-delete-directory tmp-dir)) + + ;; output archive-contents + (elpamr--log-message "Creating archive-contents...") + (with-temp-buffer + (let* ((print-level nil) + (print-length nil)) + (insert "(1\n") + (dolist (final-pkg final-pkg-list) + ;; each package occupies one line + (insert (elpamr--one-item-for-archive-contents final-pkg))) + (insert ")")) + (write-file (elpamr--fullpath output-directory "archive-contents"))) + (run-hook-with-args 'elpamr-finished-hook output-directory) + (elpamr--log-message "DONE! Output directory: %s" output-directory)))) + +(provide 'elpa-mirror) +;;; elpa-mirror.el ends here |
