;;;; -*- Mode: Lisp; indent-tabs-mode: nil -*- ;;; ;;; ediware.lisp --- Luis Oliveira ;;; ;;; This code is placed in the public domain by the author with ;;; absolutely no warranty. #+:sbcl (eval-when (:compile-toplevel :load-toplevel :execute) (require :asdf)) #+(or) ;for now, assume they are already loaded (eval-when (:compile-toplevel :load-toplevel :execute) (asdf:oos 'asdf:load-op :drakma) (asdf:oos 'asdf:load-op :cl-who) (asdf:oos 'asdf:load-op :net-telent-date)) (defpackage #:ediware (:use #:cl #:drakma #:cl-who) (:import-from #:date #:parse-time)) (in-package #:ediware) (defparameter +tarballs+ '((cl-webdav . "http://weitz.de/files/cl-webdav.tar.gz") (skippy . "http://www.xach.com/lisp/skippy.tgz") (salza . "http://www.xach.com/lisp/salza/salza-0.7.2.tar.gz") (trivial-sockets . "http://ftp.linux.org.uk/pub/lisp/cclan/trivial-sockets.tar.gz") (split-sequence . "http://ftp.linux.org.uk/pub/lisp/cclan/split-sequence.tar.gz") (rfc2388 . "http://common-lisp.net/project/rfc2388/rfc2388_latest.tar.gz") (parse-number . "http://common-lisp.net/project/asdf-packaging/parse-number-latest.tar.gz") (net-telent-date . "http://ftp.linux.org.uk/pub/lisp/cclan/net-telent-date.tar.gz") (ltk :page "Ltk" :prefix "http://www.peter-herth.de/ltk/ltk-") (ironclad . "http://www.method-combination.net/lisp/files/ironclad.tar.gz") (chipz . "http://www.method-combination.net/lisp/files/chipz.tar.gz") (psgraph . "http://common-lisp.net/project/asdf-packaging/psgraph-latest.tar.gz"))) (defparameter +stampfile+ #p"/project/clbuild/public_html/mirror/ediware-timestamps") (defparameter +tmptarball+ #p"/project/clbuild/public_html/mirror/tmptarball.tgz") (defparameter +darcs-dir+ #p"/project/clbuild/public_html/mirror/") (defparameter +report-file+ #p"/project/clbuild/public_html/mirror/index.html") (defparameter +base-url+ "http://common-lisp.net/project/clbuild/mirror/") ;; list of (name universal-time version-string) (defvar *timestamps* nil) (defun get-url (name) (cdr (assoc name +tarballs+))) (defun update-version (name version) (let ((entry (assoc name *timestamps*))) (setf (cdr entry) (list (second entry) version)))) (defun update-time (name time) (let ((entry (assoc name *timestamps*))) (setf (cdr entry) (list time (third entry))))) (defun last-modified (url) (let ((headers (nth-value 2 (http-request url :method :head)))) (parse-time (cdr (assoc :last-modified headers))))) (defun poll-tarballs () (loop for (name . url) in +tarballs+ collect (list name (last-modified (uri-from-spec url)) "initial version"))) (defun save-timestamps () (with-open-file (out +stampfile+ :direction :output :if-exists :supersede) (prin1 *timestamps* out))) (defun load-timestamps () (with-open-file (in +stampfile+ :direction :input) (let ((*package* (find-package '#:ediware)) (*read-eval* nil)) (setq *timestamps* (read in))))) (defun collect-changed (old new) (loop for (name . time) in new unless (eql (second (assoc name old)) time) collect name)) (defun harmless-uri-character-p (c) (or (alpha-char-p c) (digit-char-p c) (find c "-_."))) (defun get-url-from-cliki (spec) (destructuring-bind (&key page prefix) spec (multiple-value-bind (ignore code plist) (drakma:http-request (format nil "http://www.cliki.net/~A?download" page) :redirect nil) (declare (ignore ignore)) (check-type code (eql 302)) (let ((url (cdr (assoc :location plist)))) (unless (and (eql (mismatch prefix url) (length prefix)) (null (position-if-not #'harmless-uri-character-p url :start (length prefix)))) (error "url for ~A doesn't match prefix ~A: ~A" page prefix url)) url)))) (defun uri-from-spec (spec) (if (consp spec) (get-url-from-cliki spec) spec)) (defun grab-tarball (url) (let ((stream (http-request (uri-from-spec url) :want-stream t))) (with-open-file (out +tmptarball+ :direction :output :if-exists :supersede :element-type '(unsigned-byte 8)) (loop for byte = (read-byte stream nil nil) while byte do (write-byte byte out))))) (defun unpack-tarball (dest) (asdf:run-shell-command "tar --strip-path=1 -C ~A -xzf ~A" dest +tmptarball+)) (defun darcs-dir (name) (let ((subdir (list :relative (string-downcase (symbol-name name))))) (merge-pathnames (make-pathname :directory subdir) +darcs-dir+))) (defun find-file (path &rest alternatives) (loop for alt in alternatives for ret = (probe-file (merge-pathnames alt path)) when ret do (return ret))) (defun get-version (path) #+nil (let ((file (find-file path "CHANGELOG" "CHANGELOG.txt"))) (with-open-file (in file :direction :input) (string-downcase (read-line in)))) (write-to-string (get-universal-time))) (defun darcs-init (path) (asdf:run-shell-command "cd ~A; darcs init " path)) (defun darcs-register (path message) (asdf:run-shell-command "cd ~A; darcs record -la -m \"~A\" -A ediware" path message)) (defun format-date (time) (date:universal-time-to-rfc2822-date time)) (defun output-report () (with-open-file (out +report-file+ :direction :output :if-exists :supersede) (with-html-output (out nil :prologue t) (:html (:head (:title "Mirror")) (:body (:h1 "Mirror") (:p "The following is a collection of darcs trees nightly generated by " (:a :href "work/ediware.lisp" "this program") " (stolen from ~loliveira) for some of the tarballs required by clbuild.") (:p "Last update: " (str (format-date (get-universal-time)))) (:ul (loop for (name time version) in *timestamps* for lname = (string-downcase (symbol-name name)) do (htm (:li (:p (:a :href "#fixme" (str name)) " — " (str version) " — last changed: " (str (format-date time)) (:br) (let ((url (format nil "~A~A" +base-url+ lname))) (htm (:code (:a :href url (str url))))))))))))))) (defun first-run () (setq *timestamps* (poll-tarballs)) (loop for (name . url) in +tarballs+ for dir = (darcs-dir name) do (format t "Grabbing ~A onto ~A...~%" name dir) (ensure-directories-exist dir) (grab-tarball url) (unpack-tarball dir) (darcs-init dir) (let ((version (get-version dir))) (darcs-register dir version) (update-version name version))) (save-timestamps) (output-report)) ;; don't use ignore-errors like this at home... (defmacro ignore-errors* (&body body) `(handler-case (progn ,@body) (error (c) (warn "ignoring ~A" c)))) (defun run () (load-timestamps) (let ((new-stamps (ignore-errors* (poll-tarballs)))) (loop for name in (collect-changed *timestamps* new-stamps) for dir = (darcs-dir name) do (ignore-errors* (format t "Updating ~A...~%" name) (grab-tarball (get-url name)) (unpack-tarball dir) (let ((version (get-version dir))) (darcs-register dir version) (update-version name version)) (update-time name (second (assoc name new-stamps)))))) (save-timestamps) (output-report) (ignore-errors* (delete-file +tmptarball+))) (trace collect-changed) (trace poll-tarballs) (trace get-version) (trace darcs-register) (trace darcs-init) (trace update-version) (trace update-time) (trace asdf:run-shell-command) (trace get-url-from-cliki)