Branch :
;; repo
;; Copyright 2016-2022 kmx.io <contact@kmx.io>
;;
;; Permission is hereby granted to use this software granted
;; the above copyright notice and this permission paragraph
;; are included in all copies and substantial portions of this
;; software.
;;
;; THIS SOFTWARE IS PROVIDED "AS-IS" WITHOUT ANY GUARANTEE OF
;; PURPOSE AND PERFORMANCE. IN NO EVENT WHATSOEVER SHALL THE
;; AUTHOR BE CONSIDERED LIABLE FOR THE USE AND PERFORMANCE OF
;; THIS SOFTWARE.
(in-package :common-lisp-user)
(defpackage :repo
(:use :common-lisp)
(:export #:boot
#:clear-repos
#:find-repo
#:find-repo-by-package
#:git
#:github
#:index
#:index!
#:*index*
#:index-repos
#:install
#:kmx
#:*log-commands*
#:repo
#:repo!
#:*repos*
#:run-program
#:sh
#:sh-quote
#:str
#:sysdef
#:update))
(defpackage :repo-user
(:use :common-lisp :repo))
(in-package :repo)
;; variables
(defvar *index*)
(defvar *log-commands* t)
(defvar *repos* ())
;; string functions
(defvar *spaces* (coerce '(#\Space #\Tab) 'string))
(defun string-starts-with (x string)
(let ((lx (length x))
(ls (length string)))
(when (and (>= ls lx)
(string= x string :end2 lx))
lx)))
(defun string-ends-with (x string)
(let* ((lx (length x))
(ls (length string))
(dl (- ls lx)))
(when (and (>= ls lx)
(string= x string :start2 dl))
dl)))
(defun string-split (s x)
(let ((p (search s x)))
(if p
(cons (subseq x 0 p)
(string-split s (subseq x (+ (length s) p))))
(cons x nil))))
(defun first-line (x)
(let ((newline (position #\Newline x)))
(if newline
(subseq x 0 newline)
x)))
(defun dirname (x)
(let ((slash (position #\/ x :from-end t
:end (or (string-ends-with "/" x) (length x)))))
(cond ((null slash) "")
((= 0 slash) "/")
(t (subseq x 0 slash)))))
(defun basename (x)
(let* ((end (or (string-ends-with "/" x) (length x)))
(slash (position #\/ x :from-end t :end end)))
(cond ((null slash) (subseq x 0 end))
(t (subseq x (1+ slash) end)))))
(defun probe-dir (x)
#+clisp (ext:probe-directory (format nil "~A/" x))
#-clisp (probe-file (format nil "~A/" x)))
(defun str (&rest parts)
(labels ((to-str (x)
(typecase x
(string x)
(null "")
(cons (apply 'str x))
(pathname (namestring x))
(t (prin1-to-string x)))))
(apply 'concatenate 'string (mapcar #'to-str parts))))
(defun kw (x)
(intern (string-upcase x) (find-package :keyword)))
(defun translate-home (x)
(if (string-starts-with "~/" x)
(str (user-homedir-pathname) (subseq x 2))
x))
;; shell commands
#+sbcl
(defun run-program (cmd &rest args)
(when *log-commands*
(format t "~&$ ~S~{ ~S~}~%" cmd args)
(force-output))
(let* ((out (make-string-output-stream))
(err (make-string-output-stream)))
(let* ((process (sb-ext:run-program cmd args
:output out
:error err
:external-format :utf-8))
(exit-code (sb-ext:process-exit-code process)))
(close out)
(close err)
(let ((out (get-output-stream-string out))
(err (get-output-stream-string err)))
(format t "~&~S~&" out)
(format t "~&~S~&" err)
(unless (= 0 exit-code)
(with-simple-restart (continue "Ignore command error")
(error "~&$ ~S~{ ~S~}~%~S" cmd args err)))
(values out err exit-code)))))
#+clisp
(defun run-program (cmd &rest args)
(when *log-commands*
(format t "~&$ ~A~{ ~A~}~%" cmd args)
(force-output))
(let* ((buf (make-array '(4096) :element-type 'character))
(stream (ext:run-program cmd :arguments args
:output :stream :wait t))
(len (read-sequence buf stream))
(out (subseq buf 0 len)))
(format t "~&~A~&" out)
(values out "" 0)))
(defun sh (&rest parts)
(let ((cmd (str parts)))
(when *log-commands*
(format t "~&$ ~A~%" cmd)
(force-output))
(let ((*log-commands* nil))
(run-program "/bin/sh" "-c" cmd))))
(defvar *sh-unquoted-chars*
"+,-./0123456789:=ABCDEFGHIJKLMNOPQRSTUVWXYZ^_abcdefghijklmnopqrstuvwxyz")
(defvar *sh-quoted-chars*
"\"$\\`")
(defun sh-need-quote (x)
(dotimes (i (length x))
(unless (find (char x i) *sh-unquoted-chars*)
(return t))))
(defun sh-quote (x)
(if (sh-need-quote x)
(with-output-to-string (out)
(write-char #\" out)
(dotimes (i (length x))
(let ((c (char x i)))
(when (find c *sh-quoted-chars*)
(write-char #\\ out))
(write-char c out)))
(write-char #\" out))
x))
(defun sh-quote-dir (x)
(let ((home (string-starts-with "~/" x)))
(if home
(str "~/" (sh-quote (subseq x home)))
(sh-quote x))))
;; property list functions
(defun plist-merge (to add &rest more-lists)
(cond
((endp add)
(if (endp more-lists)
to
(plist-merge to
(first more-lists)
(rest more-lists))))
((endp (rest add))
(error "Incomplete property list"))
(t
(setf (getf to (first add))
(first (rest add)))
(plist-merge to (rest (rest add))))))
;; classes
(defclass index ()
((write-date :initarg :write-date
:accessor index-write-date
:type rational)
(dir :initarg :dir
:reader index-dir
:type string)
(repos :initarg :repos
:accessor index-repos
:type list)))
(defclass repo ()
((dir :initarg :dir
:reader repo-dir
:type string)
(name :initarg :name
:reader repo-name
:type string)
(head :initarg :head
:type string)
(uri :initarg :uri
:reader repo-uri
:type string)
(url :initarg :url
:reader repo-url
:type string)
(local-dir :initarg :local-dir
:reader repo-local-dir
:type string)
(packages :initarg :packages
:reader repo-packages
:type list)
(index :initarg :index
:reader repo-index
:type index)))
(defclass git-repo (repo) ())
(defclass github-repo (git-repo) ())
(defclass kmx-repo (git-repo) ())
;; generic functions
(defgeneric install (repo))
(defgeneric update (repo))
;; repo
(defgeneric repo-asd (repo &optional package))
(defgeneric repo-dir/name (repo))
(defgeneric repo-head (repo))
(defgeneric repo-head-default (repo))
(defgeneric repo-local-file (repo &rest parts))
(defgeneric repo-package-p (x repo))
(defmethod print-object ((obj repo) stream)
(print-unreadable-object (obj stream :type t :identity t)
(with-slots (dir name uri local-dir packages) obj
(format stream "~A/~A ~S ~S ~S" dir name uri local-dir
(when (slot-boundp obj 'packages)
packages)))))
(defmethod repo-asd ((repo repo) &optional
(package (first (repo-packages repo))))
(let ((found (first
(directory
(str (translate-home (repo-local-dir repo)) "/**/"
(string-downcase package) ".asd")))))
(when found
(namestring found))))
(defun repo-by-url (url)
(find url *repos* :key #'repo-url :test #'string=))
(defun repo-by-uri (uri)
(find uri *repos* :key #'repo-uri :test #'string=))
(defmethod repo-dir/name ((repo repo))
(str (repo-dir repo) "/" (repo-name repo)))
(defmethod repo-head ((repo repo))
(if (slot-boundp repo 'head)
(slot-value repo 'head)
(repo-head-default repo)))
(defmethod repo-local-file ((repo repo) &rest parts)
(str (repo-local-dir repo) "/" parts))
(defmethod repo-package-p (x repo)
(find x (repo-packages repo) :test #'string-equal))
;; git
(defvar *git*
(or (probe-file "/usr/bin/git")
(probe-file "/usr/local/bin/git")
(first-line (sh "which git"))))
(defun $git (&rest args)
(apply 'run-program *git* args))
;; git repo
(defgeneric $git-checkout (repo))
(defgeneric $git-clone (repo))
(defgeneric $git-fetch (repo))
(defgeneric $git-pull (repo))
(defmethod $git-checkout ((repo git-repo))
(let* ((local (repo-local-dir repo))
(head (repo-head repo))
(str-head (str head))
(args `("-C" ,(translate-home local) "checkout"
,@(unless (= 0 (length str-head))
'(str-head)))))
(apply #'$git args)
nil))
(defmethod $git-clone ((repo git-repo))
(let ((local (repo-local-dir repo))
(url (repo-url repo)))
(when (probe-dir local)
(error "git clone: not overwriting existing local directory~&~S" local))
(let ((parent (dirname local)))
(ensure-directories-exist (str parent "/") :verbose t)
($git "-C" (translate-home parent) "clone" url)
nil)))
(defmethod $git-fetch ((repo git-repo))
(let ((local (repo-local-dir repo)))
($git "-C" (translate-home local) "fetch")
nil))
(defmethod $git-pull ((repo git-repo))
(let ((local (repo-local-dir repo)))
($git "-C" (translate-home local) "pull")
nil))
(defmethod install ((repo git-repo))
(let ((local (repo-local-dir repo)))
(unless (probe-dir local)
($git-clone repo))
(let ((asd (repo-asd repo)))
(when asd
(asdf::load-asd asd)))))
(defmethod repo-head-default ((repo git-repo))
"master")
(defmethod update ((repo git-repo))
(when (probe-dir (repo-local-dir repo))
($git-pull repo)))
(defun git-repo-uri-handler (uri &key dir &allow-other-keys)
(let ((uri (first (string-split "#" uri))))
(let ((start (or (string-starts-with "git://" uri)
(string-starts-with "http://" uri)
(string-starts-with "https://" uri))))
(when start
(let* ((dot (search ".git" uri :from-end t))
(slash (position #\/ uri :end dot :from-end t))
(slash2 (position #\/ uri :end slash :from-end t))
(dir (or dir (subseq uri (1+ slash2) slash)))
(name (subseq uri (1+ slash) dot)))
`(git-repo :dir ,dir
:index ,*index*
:name ,name
:uri ,uri
:url ,uri))))))
(defun git (url &rest initargs)
(or (repo-by-url url)
(let ((repo (apply #'make-instance
(append (apply #'git-repo-uri-handler
url
initargs)
initargs))))
(push repo *repos*)
repo)))
;; github repo
(defmethod print-object ((obj github-repo) stream)
(print-unreadable-object (obj stream :type t :identity t)
(with-slots (dir name local-dir packages) obj
(format stream "~A/~A ~S ~S" dir name local-dir packages))))
(defun github-uri (user name &optional head package)
(str "github:" user "/" name
(when head "?") head
(when package "#") package))
(defun github-url (user name)
(str "https://github.com/" user "/" name ".git"))
(defun github-repo-uri-handler (uri &key dir &allow-other-keys)
(let ((uri (first (string-split "#" uri))))
(let ((start (or (string-starts-with "github:" uri)
(string-starts-with "git://github.com/" uri)
(string-starts-with "http://github.com/" uri)
(string-starts-with "https://github.com/" uri))))
(when start
(let* ((slash (or (position #\/ uri :start start)
(error "Invalid repo uri ~S" uri)))
(dot (or (string-ends-with ".git/" uri)
(string-ends-with ".git" uri)
(string-ends-with "/" uri)))
(user (subseq uri start slash))
(name (subseq uri (1+ slash) dot)))
`(github-repo :dir ,(or dir user)
:index ,*index*
:name ,name
:uri ,(github-uri user name)
:url ,(github-url user name)))))))
(defun github (user name &rest initargs &key dir &allow-other-keys)
(let ((uri (github-uri user name)))
(or (repo-by-uri uri)
(let ((repo (apply #'make-instance 'github-repo
:dir (or dir user)
:index *index*
:name name
:uri uri
:url (github-url user name)
initargs)))
(push repo *repos*)
repo))))
;; kmx repo
(defmethod print-object ((obj kmx-repo) stream)
(print-unreadable-object (obj stream :type t :identity t)
(with-slots (dir name local-dir packages) obj
(format stream "~A/~A ~S ~S" dir name local-dir packages))))
(defun kmx-uri (dir name &optional tree package)
(str "kmx:" dir "/" name
(when tree "?") tree
(when package "#") package))
(defun kmx-url (dir name)
(str "https://git.kmx.io/" dir "/" name ".git"))
(defun kmx (dir name &rest initargs)
(let ((uri (kmx-uri dir name)))
(or (repo-by-uri uri)
(let ((repo (apply #'make-instance 'kmx-repo
:dir dir
:index *index*
:name name
:uri uri
:url (kmx-url dir name)
initargs)))
(push repo *repos*)
repo))))
;; repo uri handler
(defparameter *repo-uri-handlers*
'(github-repo-uri-handler
git-repo-uri-handler))
(defun clear-repos ()
(setf *repos* nil))
(defun find-repo (uri)
(let ((uri (string uri)))
(or (find uri *repos* :key 'repo-uri :test 'string=)
(if (position #\/ uri)
(find uri *repos* :key 'repo-dir/name :test 'string-equal)
(find uri *repos* :key 'repo-name :test 'string-equal)))))
(defun find-repo-by-package (x)
(find x *repos* :test #'repo-package-p))
(defun uri-fragment (x)
(second (string-split "#" x)))
(defun repo (uri)
"Factory function for repository classes using *REPO-URI-HANDLERS*."
(when (symbolp uri)
(setq uri (symbol-name uri)))
(destructuring-bind (uri &rest packages) (string-split " " uri)
(or (find-repo uri)
(when (stringp uri)
(labels ((do-handlers (handlers)
(when handlers
(or (funcall (first handlers) uri)
(do-handlers (rest handlers))))))
(let ((spec (do-handlers *repo-uri-handlers*)))
(when spec
(let* ((class (first spec))
(initargs (rest spec))
(uri (getf initargs :uri))
(kw (kw (getf initargs :name)))
(initargs (plist-merge initargs
`(:packages ,(or packages
`(,kw))))))
(or (find-repo uri)
(let ((repo (apply 'make-instance class initargs)))
(push repo *repos*)
repo))))))))))
(defun repo! (x)
(or (repo x)
(error "unknown repository : ~S" x)))
(defmethod $git-clone ((uri string))
($git-clone (repo! uri)))
(defmethod $git-pull ((uri string))
($git-pull (repo! uri)))
;; repos list
(defmethod install ((repos cons))
(map nil 'install repos))
(defmethod update ((repos cons))
(map nil 'update repos))
;; index
(defgeneric index-file (index))
(defgeneric reload-index (index))
(defgeneric maybe-reload-index (index))
(defmethod print-object ((obj index) stream)
(print-unreadable-object (obj stream :type t :identity t)
(format stream "~S ~A repos"
(index-file obj)
(length (index-repos obj)))))
(defmethod index-file ((index index))
(str (index-dir index) "/repo-index.lisp"))
(defun index-from-file (pathname)
(let* ((*repos* nil)
(write-date (file-write-date pathname))
(*index* (make-instance 'index
:write-date write-date
:dir (dirname pathname)
:repos *repos*)))
(load pathname)
(setf (index-repos *index*) *repos*)
*index*))
(defmethod reload-index ((index index))
(let* ((pathname (index-file index))
(*repos* nil)
(write-date (file-write-date pathname)))
(load pathname)
(setf (index-write-date index) write-date
(index-repos index) *repos*))
index)
(defmethod maybe-reload-index ((index index))
(if (< (index-write-date index)
(file-write-date (index-file index)))
(reload-index index)
index))
(defmethod install ((index index))
(let ((index (maybe-reload-index index)))
(let ((*repos* (index-repos index)))
(install *repos*))))
(defmethod update ((index index))
(let ((index (maybe-reload-index index)))
(let ((*repos* (index-repos index)))
(update *repos*))))
;; index uri handlers
(defun index-file-p (x)
(or (string= "repo-index.lisp" x)
(string-ends-with "/repo-index.lisp" x)))
(defun local-index-uri-handler (x)
(let ((end (string-ends-with "/repo-index.lisp" x)))
(when end
(let* ((dir (subseq x 0 end))
(index (str dir "/repo-index.lisp")))
(when (probe-file index)
(index-from-file index))))))
(defvar *index-uri-handlers*
'(local-index-uri-handler))
(defun index (uri)
"Load index from uri"
(labels ((do-handlers (handlers)
(unless (endp handlers)
(or (funcall (first handlers) uri)
(do-handlers (rest handlers))))))
(do-handlers *index-uri-handlers*)))
(defun index! (uri)
(or (index uri) (error "failed to load index ~S" uri)))
;; repo
(defmethod initialize-instance :after ((repo repo) &rest initargs)
(declare (ignore initargs))
(with-slots (dir index name packages) repo
(setf (slot-value repo 'local-dir)
(format nil "~A/~A/~A" (index-dir index) dir name))
(unless (slot-boundp repo 'packages)
(setf packages (list name)))))
;; install and update commands
(defmethod install ((x string))
(when *index*
(maybe-reload-index *index*)
(setq *repos* (index-repos *index*)))
(if (index-file-p x)
(install (index! x))
(install (repo! x))))
(defmethod install ((x null))
nil)
(defmethod install ((x symbol))
(when *index*
(maybe-reload-index *index*)
(setq *repos* (index-repos *index*)))
(install (repo! x)))
(defmethod update ((x string))
(when *index*
(maybe-reload-index *index*)
(setq *repos* (index-repos *index*)))
(if (index-file-p x)
(update (index! x))
(update (repo! x))))
(defmethod update ((x null))
nil)
(defmethod update ((x symbol))
(when *index*
(maybe-reload-index *index*)
(setq *repos* (index-repos *index*)))
(update (repo! x)))
;; system-definition
(defun sysdef (x sysdef-file)
(declare (type function sysdef-file))
(when *index*
(maybe-reload-index *index*)
(setq *repos* (index-repos *index*)))
(let ((repo (or (find-repo-by-package x)
(repo x))))
(when repo
(install repo)
(pathname (funcall sysdef-file repo x)))))
(defun sysdef-asdf (x)
(sysdef x #'repo-asd))
;; start repo : load index and link with ASDF
(defun boot (&optional (dir "~/common-lisp"))
(let ((index-file (str dir "/repo-index.lisp")))
(when (probe-file index-file)
(setq *index* (index index-file))
(setq *repos* (index-repos *index*))
(when (find-package :asdf)
(pushnew 'sysdef-asdf
(symbol-value
(intern "*SYSTEM-DEFINITION-SEARCH-FUNCTIONS*"
:asdf)))))))