release.lisp 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. #!/usr/bin/env clisp
  2. ;;;; -*- Mode: lisp; indent-tabs-mode: nil -*-
  3. (defpackage :release-script (:use #:cl #:regexp))
  4. (in-package :release-script)
  5. ;;;; Configuration ------------------------------------------------------------
  6. (defparameter *project-name* "cffi")
  7. (defparameter *asdf-file* (format nil "~A.asd" *project-name*))
  8. (defparameter *host* "common-lisp.net")
  9. (defparameter *release-dir*
  10. (format nil "/project/~A/public_html/releases" *project-name*))
  11. (defparameter *version-file* "VERSION")
  12. (defparameter *version-file-dir*
  13. (format nil "/project/~A/public_html" *project-name*))
  14. ;;;; --------------------------------------------------------------------------
  15. ;;;; Utilities
  16. (defun ensure-list (x)
  17. (if (listp x) x (list x)))
  18. (defmacro string-case (expression &body clauses)
  19. `(let ((it ,expression)) ; yes, anaphoric, deal with it.
  20. (cond
  21. ,@(loop for clause in clauses collect
  22. `((or ,@(loop for alternative in (ensure-list (first clause))
  23. collect (or (eq t alternative)
  24. `(string= it ,alternative))))
  25. ,@(rest clause))))))
  26. (defparameter *development-mode* nil)
  27. (defun die (format-control &rest format-args)
  28. (format *error-output* "~?" format-control format-args)
  29. (if *development-mode*
  30. (cerror "continue" "die")
  31. (ext:quit 1)))
  32. (defun numeric-split (string)
  33. (if (digit-char-p (char string 0))
  34. (multiple-value-bind (number next-position)
  35. (parse-integer string :junk-allowed t)
  36. (cons number (when (< next-position (length string))
  37. (numeric-split (subseq string next-position)))))
  38. (let ((next-digit-position (position-if #'digit-char-p string)))
  39. (if next-digit-position
  40. (cons (subseq string 0 next-digit-position)
  41. (numeric-split (subseq string next-digit-position)))
  42. (list string)))))
  43. (defun natural-string-< (s1 s2)
  44. (labels ((aux< (l1 l2)
  45. (cond ((null l1) (not (null l2)))
  46. ((null l2) nil)
  47. (t (destructuring-bind (x . xs) l1
  48. (destructuring-bind (y . ys) l2
  49. (cond ((and (numberp x) (stringp y))
  50. t)
  51. ((and (numberp y) (stringp x))
  52. nil)
  53. ((and (numberp x) (numberp y))
  54. (or (< x y) (and (= x y) (aux< xs ys))))
  55. (t
  56. (or (string-lessp x y)
  57. (and (string-equal x y)
  58. (aux< xs ys)))))))))))
  59. (aux< (numeric-split s1)
  60. (numeric-split s2))))
  61. ;;;; Running commands
  62. (defparameter *dry-run* nil)
  63. (defun cmd? (format-control &rest format-args)
  64. (let ((cmd (format nil "~?" format-control format-args)))
  65. (with-open-stream (s1 (ext:run-shell-command cmd :output :stream))
  66. (loop for line = (read-line s1 nil nil)
  67. while line
  68. collect line))))
  69. ;; XXX: quote arguments.
  70. (defun cmd (format-control &rest format-args)
  71. (when *development-mode*
  72. (format *debug-io* "CMD: ~?~%" format-control format-args))
  73. (let ((ret (ext:run-shell-command (format nil "~?" format-control format-args))))
  74. (or (null ret)
  75. (zerop ret))))
  76. (defun cmd! (format-control &rest format-args)
  77. (or (apply #'cmd format-control format-args)
  78. (die "cmd '~?' failed." format-control format-args)))
  79. (defun maybe-cmd! (format-control &rest format-args)
  80. (if *dry-run*
  81. (format t "SUPPRESSING: ~?~%" format-control format-args)
  82. (apply #'cmd! format-control format-args)))
  83. ;;;;
  84. (defun find-current-version ()
  85. (subseq (reduce (lambda (x y) (if (natural-string-< x y) y x))
  86. (or (cmd? "git tag -l v\\*")
  87. (die "no version tags found. Please specify initial version.")))
  88. 1))
  89. (defun parse-version (string)
  90. (mapcar (lambda (x)
  91. (parse-integer x :junk-allowed t))
  92. (loop repeat 3 ; XXX: parameterize
  93. for el in (regexp-split "\\." (find-current-version))
  94. collect el)))
  95. (defun check-for-unrecorded-changes (&optional force)
  96. (unless (cmd "git diff --exit-code")
  97. (write-line "Unrecorded changes.")
  98. (if force
  99. (write-line "Continuing anyway.")
  100. (die "Aborting.~@
  101. Use -f or --force if you want to make a release anyway."))))
  102. (defun new-version-number-candidates (current-version)
  103. (labels ((alternatives (before after)
  104. (when after
  105. (cons (append before
  106. (list (1+ (first after)))
  107. (mapcar (constantly 0) (rest after)))
  108. (alternatives (append before (list (first after)))
  109. (rest after))))))
  110. (loop for alt in (alternatives nil (parse-version current-version))
  111. collect (format nil "~{~d~^.~}" alt))))
  112. (defun ask-user-for-version (current-version next-versions)
  113. (format *query-io* "Current version is ~A. Which will be the next one?~%"
  114. current-version)
  115. (loop for i from 1 and version in next-versions
  116. do (format *query-io* "~T~A) ~A~%" i version))
  117. (format *query-io* "? ")
  118. (finish-output *query-io*)
  119. (nth (1- (parse-integer (read-line) :junk-allowed t))
  120. next-versions))
  121. (defun git-tag-tree (version)
  122. (write-line "Tagging the tree...")
  123. (maybe-cmd! "git tag \"v~A\"" version))
  124. (defun add-version-to-system-file (version path-in path-out)
  125. (let ((defsystem-line (format nil "(defsystem :~A" *project-name*)))
  126. (with-open-file (in path-in :direction :input)
  127. (with-open-file (out path-out :direction :output)
  128. (loop for line = (read-line in nil nil) while line
  129. do (write-line line out)
  130. when (string= defsystem-line line)
  131. do (format out " :version ~s~%" version))))))
  132. (defun create-dist (version distname)
  133. (write-line "Creating distribution...")
  134. (cmd! "mkdir \"~a\"" distname)
  135. (cmd! "git archive master | tar xC \"~A\"" distname)
  136. (format t "Updating ~A with new version: ~A~%" *asdf-file* version)
  137. (let* ((asdf-file-path (format nil "~A/~A" distname *asdf-file*))
  138. (tmp-asdf-file-path (format nil "~a.tmp" asdf-file-path)))
  139. (add-version-to-system-file version asdf-file-path tmp-asdf-file-path)
  140. (cmd! "mv \"~a\" \"~a\"" tmp-asdf-file-path asdf-file-path)))
  141. (defun tar-and-sign (distname tarball)
  142. (write-line "Creating and signing tarball...")
  143. (cmd! "tar czf \"~a\" \"~a\"" tarball distname)
  144. (cmd! "gpg -b -a \"~a\"" tarball))
  145. (defparameter *remote-directory* (format nil "~A:~A" *host* *release-dir*))
  146. (defun upload-tarball (tarball signature remote-directory)
  147. (write-line "Copying tarball to web server...")
  148. (maybe-cmd! "scp \"~A\" \"~A\" \"~A\"" tarball signature remote-directory)
  149. (format t "Uploaded ~A and ~A.~%" tarball signature))
  150. (defun update-remote-links (tarball signature host release-dir project-name)
  151. (format t "Updating ~A_latest links...~%" project-name)
  152. (maybe-cmd! "ssh \"~A\" ln -sf \"~A\" \"~A/~A_latest.tar.gz\""
  153. host tarball release-dir project-name)
  154. (maybe-cmd! "ssh \"~A\" ln -sf \"~A\" \"~A/~A_latest.tar.gz.asc\""
  155. host signature release-dir project-name))
  156. (defun upload-version-file (version version-file host version-file-dir)
  157. (format t "Uploading ~A...~%" version-file)
  158. (with-open-file (out version-file :direction :output)
  159. (write-string version out))
  160. (maybe-cmd! "scp \"~A\" \"~A\":\"~A\"" version-file host version-file-dir)
  161. (maybe-cmd! "rm \"~A\"" version-file))
  162. (defun maybe-clean-things-up (tarball signature)
  163. (when (y-or-n-p "Clean local tarball and signature?")
  164. (cmd! "rm \"~A\" \"~A\"" tarball signature)))
  165. (defun run (force version)
  166. (check-for-unrecorded-changes force)
  167. ;; figure out what version we'll be preparing.
  168. (unless version
  169. (let* ((current-version (find-current-version))
  170. (next-versions (new-version-number-candidates current-version)))
  171. (setf version (or (ask-user-for-version current-version next-versions)
  172. (die "invalid selection.")))))
  173. (git-tag-tree version)
  174. (let* ((distname (format nil "~A_~A" *project-name* version))
  175. (tarball (format nil "~A.tar.gz" distname))
  176. (signature (format nil "~A.asc" tarball)))
  177. ;; package things up.
  178. (create-dist version distname)
  179. (tar-and-sign distname tarball)
  180. ;; upload.
  181. (upload-tarball tarball signature *remote-directory*)
  182. (update-remote-links tarball signature *host* *release-dir* *project-name*)
  183. (when *version-file*
  184. (upload-version-file version *version-file* *host* *version-file-dir*))
  185. ;; clean up.
  186. (maybe-clean-things-up tarball signature)
  187. ;; documentation.
  188. (write-line "Building and uploading documentation...")
  189. (maybe-cmd! "make -C doc upload-docs")
  190. ;; push tags and any outstanding changes.
  191. (write-line "Pushing tags and changes...")
  192. (maybe-cmd! "git push --tags origin master")))
  193. ;;;; Do it to it
  194. (let ((force nil)
  195. (version nil)
  196. (args ext:*args*))
  197. (loop while args
  198. do (string-case (pop args)
  199. (("-h" "--help")
  200. (write-line "No help, sorry. Read the source.")
  201. (ext:quit 0))
  202. (("-f" "--force")
  203. (setf force t))
  204. (("-v" "--version")
  205. (setf version (pop args)))
  206. (("-n" "--dry-run")
  207. (setf *dry-run* t))
  208. (t
  209. (die "Unrecognized argument '~a'" it))))
  210. (run force version))