├── lem-extension-manager.asd ├── source.lisp ├── LICENSE ├── quicklisp.lisp ├── README.md └── main.lisp /lem-extension-manager.asd: -------------------------------------------------------------------------------- 1 | (defsystem "lem-extension-manager" 2 | :description "Configuration library for managing Lem packages." 3 | :license "MIT" 4 | :depends-on (:alexandria) 5 | :serial t 6 | :components ((:file "source") 7 | (:file "quicklisp" :if-feature :quicklisp) 8 | (:file "main"))) 9 | -------------------------------------------------------------------------------- /source.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :lem-extension-manager/source 2 | (:use :cl) 3 | (:export :source 4 | :download-source 5 | :local)) 6 | 7 | (in-package :lem-extension-manager/source) 8 | 9 | (defstruct source name) 10 | 11 | (defstruct (local (:include source))) 12 | 13 | (defgeneric download-source (source output-location) 14 | (:documentation "It downloads the SOURCE to the desired location.")) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 FMF 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /quicklisp.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :lem-extension-manager/quicklisp 2 | (:use :cl :uiop :lem-extension-manager/source) 3 | (:export :make-quicklisp)) 4 | 5 | (in-package :lem-extension-manager/quicklisp) 6 | 7 | (defun url-not-suitable-error-p (condition) 8 | (<= 400 (ql-http:unexpected-http-status-code condition) 499)) 9 | 10 | (defun fetch-gzipped-version (url file &key quietly) 11 | (let ((gzipped-temp (merge-pathnames "gzipped.tmp" file))) 12 | (ql-http:fetch url gzipped-temp :quietly quietly) 13 | (ql-gunzipper:gunzip gzipped-temp file) 14 | (delete-file-if-exists gzipped-temp) 15 | (probe-file file))) 16 | 17 | (defun maybe-fetch-tgzipped (url file &key quietly) 18 | (handler-case 19 | (fetch-gzipped-version url file :quietly quietly) 20 | (ql-http:unexpected-http-status (condition) 21 | (cond ((url-not-suitable-error-p condition) 22 | (ql-http:fetch url file :quietly quietly) 23 | (probe-file file)) 24 | (t 25 | (error condition)))))) 26 | 27 | (defstruct (quicklisp (:include source))) 28 | 29 | (defvar *quicklisp-system-list* 30 | (remove-duplicates 31 | (mapcar #'ql-dist:release (ql:system-list)))) 32 | 33 | (defmethod download-source ((source quicklisp) (output-location String)) 34 | (let* ((ql:*local-project-directories* (list *packages-directory*)) 35 | (output-dir (concatenate 'string 36 | (namestring *packages-directory*) output-location)) 37 | (release (find (source-name source) 38 | *quicklisp-system-list* 39 | :key #'ql-dist:project-name 40 | :test #'string=)) 41 | (url (ql-dist:archive-url release)) 42 | (name (source-name source)) 43 | (tarfile (concatenate 'string name ".tar"))) 44 | (if release 45 | (prog1 output-dir 46 | (uiop:with-current-directory (*packages-directory*) 47 | (maybe-fetch-tgzipped url tarfile :quietly t) 48 | (ql-minitar:unpack-tarball tarfile) 49 | (delete-file tarfile) 50 | (uiop/cl:rename-file (ql-dist:prefix release) output-location))) 51 | (error "Package ~a not found!." (source-name source))))) 52 | 53 | (defun %register-maybe-quickload (name) 54 | (uiop:symbol-call :quicklisp :register-local-projects) 55 | (ql:quickload (alexandria:make-keyword name) :silent t)) 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LEM (Lem Extension Manager) 2 | 3 | It's a configuration library that add the ability to Lem to have packages withing the user configuration directory. 4 | 5 | ## Usage 6 | 7 | ### Init example 8 | The main function that we can use to add new packages is `lem-use-package`, this fuctions allow us to add libraries 9 | that will be loaded at run time when open Lem (the performance shouldn't be too much of a problem, as sbcl is very good using the cache). 10 | 11 | To use this function, we can modify our `init.lisp` file in this way: 12 | - Using the git source (we can specify a branch or specific commit): 13 | 14 | 15 | ```lisp 16 | (lem-extension-manager:lem-use-package 17 | "lisp-critic" 18 | :source (:type :git 19 | :url "https://github.com/g000001/lisp-critic.git")) 20 | 21 | (lem-extension-manager:lem-use-package "versioned-objects" 22 | :source (:type :git 23 | :url "https://github.com/smithzvk/Versioned-Objects.git" 24 | :branch "advance-versioning")) 25 | ``` 26 | - Using the quicklisp package manager directly: 27 | 28 | ```lisp 29 | 30 | (lem-extension-manager:lem-use-package "fiveam" :source (:type :quicklisp)) 31 | 32 | ``` 33 | 34 | It's also possible to recursive dependenices that are not on quicklisp (for example, other github packages), using the `:dependencies` keyword: 35 | ```lisp 36 | (lem-extension-manager:lem-use-package "versioned-objects" 37 | :source (:type :git 38 | :url "https://github.com/smithzvk/Versioned-Objects.git" 39 | :branch "advance-versioning") 40 | :dependencies (("ip-management" 41 | :source 42 | (:type :git 43 | :url "https://github.com/Sasanidas/ip-management.git")) 44 | ("1am" :source (:type :quicklisp)))) 45 | ``` 46 | 47 | ### Interactive commands 48 | 49 | LEM also provides some interactive commands to add/remove extension: 50 | 51 | - `extension-manager-install-ql-package`: 52 | 53 | This command will prompt for a list of all quicklisp indexed libraries to be installed, you can choose any of them and LEM will install it on your 54 | configuration (keep in mind that even tho it will get install/indexed for LEM, it will NOT be loaded unless specify in the configuration). 55 | 56 | 57 | - `extension-manager-test-ql-package`: 58 | 59 | This command is similar to `extension-manager-install-ql-package` with the key difference that is intended to try out quicklisp packages, so the command will download the package to a temporary directory, loaded into Lem and then forget about it. It's inteded to test packages on the fly, to make it permanent, use the `lem-use-package` function. 60 | 61 | - `extension-manager-remove-package`: 62 | 63 | This command will prompt for the installed packages and will removed the selected one. 64 | 65 | - `extension-manager-purge-packages`: 66 | 67 | This command will remove ALL installed packages. 68 | 69 | 70 | 71 | ## Configuration 72 | 73 | The packages are installed on returned folder from the lem function `lem-extension-manager::default-home` by default, but can be changed by modifing the variable `lem-extension-manager:*packages-directory*`. 74 | 75 | 76 | 77 | 78 | ## Internals 79 | TODO 80 | -------------------------------------------------------------------------------- /main.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package :lem-extension-manager 2 | (:use :cl :uiop :lem-extension-manager/source) 3 | #+quicklisp (:use-reexport :lem-extension-manager/quicklisp) 4 | (:export :*installed-packages* 5 | :*packages-directory* 6 | :lem-use-package 7 | :load-packages 8 | ;; For user commands 9 | :package-test 10 | :package-remove 11 | :packages-list 12 | 13 | :extension 14 | :extension-name 15 | :extension-source 16 | :extension-directory)) 17 | 18 | (in-package :lem-extension-manager) 19 | 20 | (defvar *installed-packages* nil) 21 | 22 | (defun default-home () 23 | (let ((xdg-lem (uiop:xdg-config-home "lem/")) 24 | (dot-lem (merge-pathnames ".lem/" (user-homedir-pathname)))) 25 | (or (uiop:getenv "LEM_HOME") 26 | (and (probe-file dot-lem) dot-lem) 27 | xdg-lem))) 28 | 29 | (defvar *packages-directory* 30 | (pathname (concatenate 'string 31 | (directory-namestring (default-home)) 32 | "packages" 33 | (string (uiop:directory-separator-for-host))))) 34 | 35 | (defstruct source name) 36 | 37 | (defstruct (local (:include source))) 38 | 39 | 40 | (defgeneric download-source (source output-location) 41 | (:documentation "It downloads the SOURCE to the desired location.")) 42 | 43 | (defvar *git-base-arglist* (list "git") 44 | "The git program, to be appended command-line options.") 45 | 46 | (defun run-git (arglist) 47 | (uiop:wait-process 48 | (uiop:launch-program (concatenate 'list *git-base-arglist* arglist) 49 | :ignore-error-status t))) 50 | 51 | (defstruct (git (:include source)) url branch commit) 52 | 53 | (defmethod download-source ((source git) (output-location String)) 54 | (let ((output-dir (concatenate 'string 55 | (namestring *packages-directory*) output-location))) 56 | (run-git (list "clone" (git-url source) output-dir)) 57 | (when (git-branch source) 58 | (uiop:with-current-directory (output-dir) 59 | (run-git (list "checkout" "-b" (git-branch source))))) 60 | 61 | (when (git-commit source) 62 | (uiop:with-current-directory (output-dir) 63 | (run-git (list "checkout" (git-commit source))))) 64 | output-dir)) 65 | 66 | (defstruct (quicklisp (:include source))) 67 | 68 | (defvar *quicklisp-system-list* 69 | (remove-duplicates 70 | (mapcar #'ql-dist:release (ql:system-list)))) 71 | 72 | (defmethod download-source ((source quicklisp) (output-location String)) 73 | (let* ((output-dir (str:concat 74 | (namestring *packages-directory*) output-location)) 75 | (release (find (source-name source) 76 | *quicklisp-system-list* 77 | :key #'ql-dist:project-name 78 | :test #'string=)) 79 | (url (ql-dist:archive-url release)) 80 | (name (source-name source)) 81 | (tgzfile (str:concat name ".tgz")) 82 | (tarfile (str:concat name ".tar"))) 83 | (if release 84 | (prog1 output-dir 85 | (uiop:with-current-directory (*packages-directory*) 86 | (quicklisp-client::maybe-fetch-gzipped url tgzfile 87 | :quietly t) 88 | (ql-gunzipper:gunzip tgzfile tarfile) 89 | (ql-minitar:unpack-tarball tarfile) 90 | (delete-file tgzfile) 91 | (delete-file tarfile) 92 | (uiop/cl:rename-file (ql-dist:prefix release) output-location))) 93 | (editor-error "Package ~a not found!." (source-name source))))) 94 | 95 | (defmethod download-source (source output-location) 96 | (editor-error "Source ~a not available." source)) 97 | 98 | 99 | (defmethod download-source (source output-location) 100 | (error "Source ~a not available." source)) 101 | 102 | (defclass extension () 103 | ((name :initarg :name 104 | :accessor extension-name) 105 | (source :initarg :source 106 | :accessor extension-source) 107 | (directory :initarg :directory 108 | :accessor extension-directory))) 109 | 110 | (defgeneric package-remove (package)) 111 | 112 | (defmethod package-remove ((package extension)) 113 | (uiop:delete-directory-tree 114 | (uiop:truename* (extension-directory package)) :validate t) 115 | (delete package *installed-packages*)) 116 | 117 | (defgeneric package-test (package)) 118 | 119 | (defmethod package-test ((package extension)) 120 | (let* ((*packages-directory* (uiop:temporary-directory)) 121 | (name (extension-name package)) 122 | (source (extension-source package))) 123 | (%download-package source name) 124 | #+quicklisp 125 | (%register-maybe-quickload (extension-name package)))) 126 | 127 | (defun packages-list () 128 | (remove-duplicates 129 | (mapcar (lambda (d) (pathname (directory-namestring d))) 130 | (directory (merge-pathnames "**/*.asd" *packages-directory*))))) 131 | 132 | (defun insert-package (package) 133 | (pushnew package *installed-packages* 134 | :test (lambda (a b) 135 | (string= 136 | (extension-name a) 137 | (extension-name b))))) 138 | 139 | (defun define-source (source-list name) 140 | (let ((s (getf source-list :type))) 141 | (ecase s 142 | (:git 143 | (destructuring-bind (&key type url branch commit) 144 | source-list 145 | (declare (ignore type)) 146 | (make-git :name name 147 | :url url 148 | :branch branch 149 | :commit commit))) 150 | #+quicklisp (:quicklisp 151 | (destructuring-bind (&key type) 152 | source-list 153 | (declare (ignore type)) 154 | (make-quicklisp :name name))) 155 | (t (error "Source ~a not available." s))))) 156 | 157 | #+quicklisp 158 | (defun %register-maybe-quickload (name) 159 | (uiop:symbol-call :quicklisp :register-local-projects) 160 | (ql:quickload (alexandria:make-keyword name) :silent t)) 161 | 162 | (defun %download-package (source name) 163 | (format t "Downloading ~a..." name) 164 | (download-source source name) 165 | (format t "Done downloading ~a!" name)) 166 | 167 | #| 168 | 169 | git source: (list :type type :url url :branch branch :commit commit :dependecies dependencie-list) 170 | 171 | dependency: (("versioned-objects" :source 172 | 173 | (lem-use-package "versioned-objects" 174 | :source (:type :git 175 | :url "https://github.com/smithzvk/Versioned-Objects.git" 176 | :branch "advance-versioning")) 177 | 178 | 179 | Quicklisp can take care of the dependencies 180 | (lem-use-package "fiveam" :source (:type :quicklisp)) 181 | 182 | |# 183 | (defmacro lem-use-package (name &key source after 184 | bind hooks 185 | force dependencies) 186 | (declare (ignore hooks bind after)) 187 | #+sbcl 188 | (ensure-directories-exist *packages-directory*) 189 | (alexandria:with-gensyms (spackage rsource pdir) 190 | `(let* ((asdf:*central-registry* 191 | (union (packages-list) 192 | asdf:*central-registry* 193 | :test #'equal)) 194 | #+quicklisp 195 | (ql:*local-project-directories* 196 | (nconc (list *packages-directory*) 197 | ql:*local-project-directories*)) 198 | (,rsource (define-source ',source ,name)) 199 | (,pdir (merge-pathnames *packages-directory* ,name)) 200 | (,spackage (make-instance 'extension 201 | :name ,name 202 | :source ,rsource 203 | :directory ,pdir))) 204 | (when (or ,force 205 | (not (uiop:directory-exists-p ,pdir))) 206 | (%download-package ,rsource ,name)) 207 | ,(when dependencies 208 | (loop for dep in dependencies 209 | do (eval `(lem-use-package ,@dep)))) 210 | (insert-package ,spackage) 211 | (and #+quicklisp (%register-maybe-quickload ,name) t)))) 212 | 213 | 214 | ;; Package util functions/commands 215 | 216 | #+quicklisp 217 | (defun load-packages () 218 | (let ((ql:*local-project-directories* (list *packages-directory*))) 219 | (loop for dpackage in (directory (merge-pathnames "*/" *packages-directory*)) 220 | for spackage = (car 221 | (last 222 | (pathname-directory 223 | (uiop:directorize-pathname-host-device dpackage)))) 224 | do (insert-package 225 | (make-instance 'extension 226 | :name spackage 227 | :source (make-local :name spackage) 228 | :directory dpackage)) 229 | do (ql:quickload (alexandria:make-keyword spackage) :silent t)))) 230 | --------------------------------------------------------------------------------