diff --git a/scripts/release.lisp b/scripts/release.lisp new file mode 100755 index 0000000000000000000000000000000000000000..51f21bc0f3e07a7f648ad7e64c79b1ab6ff9b853 --- /dev/null +++ b/scripts/release.lisp @@ -0,0 +1,250 @@ +#!/usr/bin/env clisp +;;;; -*- Mode: lisp; indent-tabs-mode: nil -*- + +(defpackage :release-script (:use #:cl #:regexp)) +(in-package :release-script) + +;;;; Configuration ------------------------------------------------------------ + +(defparameter *project-name* "cffi") +(defparameter *asdf-file* (format nil "~A.asd" *project-name*)) + +(defparameter *host* "common-lisp.net") +(defparameter *release-dir* + (format nil "/project/~A/public_html/releases" *project-name*)) + +(defparameter *version-file* "VERSION") +(defparameter *version-file-dir* + (format nil "/project/~A/public_html" *project-name*)) + +;;;; -------------------------------------------------------------------------- + +;;;; Utilities + +(defun ensure-list (x) + (if (listp x) x (list x))) + +(defmacro string-case (expression &body clauses) + `(let ((it ,expression)) ; yes, anaphoric, deal with it. + (cond + ,@(loop for clause in clauses collect + `((or ,@(loop for alternative in (ensure-list (first clause)) + collect (or (eq t alternative) + `(string= it ,alternative)))) + ,@(rest clause)))))) + +(defparameter *development-mode* nil) + +(defun die (format-control &rest format-args) + (format *error-output* "~?" format-control format-args) + (if *development-mode* + (cerror "continue" "die") + (ext:quit 1))) + +(defun numeric-split (string) + (if (digit-char-p (char string 0)) + (multiple-value-bind (number next-position) + (parse-integer string :junk-allowed t) + (cons number (when (< next-position (length string)) + (numeric-split (subseq string next-position))))) + (let ((next-digit-position (position-if #'digit-char-p string))) + (if next-digit-position + (cons (subseq string 0 next-digit-position) + (numeric-split (subseq string next-digit-position))) + (list string))))) + +(defun natural-string-< (s1 s2) + (labels ((aux< (l1 l2) + (cond ((null l1) (not (null l2))) + ((null l2) nil) + (t (destructuring-bind (x . xs) l1 + (destructuring-bind (y . ys) l2 + (cond ((and (numberp x) (stringp y)) + t) + ((and (numberp y) (stringp x)) + nil) + ((and (numberp x) (numberp y)) + (or (< x y) (and (= x y) (aux< xs ys)))) + (t + (or (string-lessp x y) + (and (string-equal x y) + (aux< xs ys))))))))))) + (aux< (numeric-split s1) + (numeric-split s2)))) + +;;;; Running commands + +(defparameter *dry-run* nil) + +(defun cmd? (format-control &rest format-args) + (let ((cmd (format nil "~?" format-control format-args))) + (with-open-stream (s1 (ext:run-shell-command cmd :output :stream)) + (loop for line = (read-line s1 nil nil) + while line + collect line)))) + +;; XXX: quote arguments. +(defun cmd (format-control &rest format-args) + (when *development-mode* + (format *debug-io* "CMD: ~?~%" format-control format-args)) + (let ((ret (ext:run-shell-command (format nil "~?" format-control format-args)))) + (or (null ret) + (zerop ret)))) + +(defun cmd! (format-control &rest format-args) + (or (apply #'cmd format-control format-args) + (die "cmd '~?' failed." format-control format-args))) + +(defun maybe-cmd! (format-control &rest format-args) + (if *dry-run* + (format t "SUPPRESSING: ~?~%" format-control format-args) + (apply #'cmd! format-control format-args))) + +;;;; + +(defun find-current-version () + (subseq (reduce (lambda (x y) (if (natural-string-< x y) y x)) + (or (cmd? "git tag -l v\\*") + (die "no version tags found. Please specify initial version."))) + 1)) + +(defun parse-version (string) + (mapcar (lambda (x) + (parse-integer x :junk-allowed t)) + (loop repeat 3 ; XXX: parameterize + for el in (regexp-split "\\." (find-current-version)) + collect el))) + +(defun check-for-unrecorded-changes (&optional force) + (unless (cmd "git diff --exit-code") + (write-line "Unrecorded changes.") + (if force + (write-line "Continuing anyway.") + (die "Aborting.~@ + Use -f or --force if you want to make a release anyway.")))) + +(defun new-version-number-candidates (current-version) + (let ((current-version (parse-version current-version))) + (labels ((alternatives (before after) + (when after + (cons (append before (list (1+ (first after))) + (mapcar (constantly 0) (rest after))) + (alternatives (append before (list (first after))) + (rest after)))))) + (loop for alt in (alternatives nil current-version) + collect (reduce (lambda (acc next) + (format nil "~a.~a" acc next)) + alt))))) + +(defun ask-user-for-version (current-version next-versions) + (format *query-io* "Current version is ~A. Which will be the next one?~%" + current-version) + (loop for i from 1 and version in next-versions + do (format *query-io* "~T~A) ~A~%" i version)) + (format *query-io* "? ") + (finish-output *query-io*) + (nth (1- (parse-integer (read-line) :junk-allowed t)) + next-versions)) + +(defun git-tag-tree (version) + (write-line "Tagging the tree...") + (maybe-cmd! "git tag \"v~A\"" version)) + +(defun add-version-to-system-file (version path-in path-out) + (let ((defsystem-line (format nil "(defsystem :~A" *project-name*))) + (with-open-file (in path-in :direction :input) + (with-open-file (out path-out :direction :output) + (loop for line = (read-line in nil nil) while line + do (write-line line out) + when (string= defsystem-line line) + do (format out " :version ~s~%" version)))))) + +(defun create-dist (version distname) + (write-line "Creating distribution...") + (cmd! "mkdir \"~a\"" distname) + (cmd! "git archive master | tar xC \"~A\"" distname) + (format t "Updating ~A with new version: ~A~%" *asdf-file* version) + (let* ((asdf-file-path (format nil "~A/~A" distname *asdf-file*)) + (tmp-asdf-file-path (format nil "~a.tmp" asdf-file-path))) + (add-version-to-system-file version asdf-file-path tmp-asdf-file-path) + (cmd! "mv \"~a\" \"~a\"" tmp-asdf-file-path asdf-file-path))) + +(defun tar-and-sign (distname tarball) + (write-line "Creating and signing tarball...") + (cmd! "tar czf \"~a\" \"~a\"" tarball distname) + (cmd! "gpg -b -a \"~a\"" tarball)) + +(defparameter *remote-directory* (format nil "~A:~A" *host* *release-dir*)) + +(defun upload-tarball (tarball signature remote-directory) + (write-line "Copying tarball to web server...") + (maybe-cmd! "scp \"~A\" \"~A\" \"~A\"" tarball signature remote-directory) + (format t "Uploaded ~A and ~A.~%" tarball signature)) + +(defun update-remote-links (tarball signature host release-dir project-name) + (format t "Updating ~A_latest links...~%" project-name) + (maybe-cmd! "ssh \"~A\" ln -sf \"~A\" \"~A/~A_latest.tar.gz\"" + host tarball release-dir project-name) + (maybe-cmd! "ssh \"~A\" ln -sf \"~A\" \"~A/~A_latest.tar.gz.asc\"" + host signature release-dir project-name)) + +(defun upload-version-file (version version-file host version-file-dir) + (format t "Uploading ~A...~%" version-file) + (with-open-file (out version-file :direction :output) + (write-string version out)) + (maybe-cmd! "scp \"~A\" \"~A\":\"~A\"" version-file host version-file-dir) + (maybe-cmd! "rm \"~A\"" version-file)) + +(defun maybe-clean-things-up (tarball signature) + (when (y-or-n-p "Clean local tarball and signature?") + (cmd! "rm \"~A\" \"~A\"" tarball signature))) + +(defun run (force version) + (check-for-unrecorded-changes force) + ;; figure out what version we'll be preparing. + (unless version + (let* ((current-version (find-current-version)) + (next-versions (new-version-number-candidates current-version))) + (setf version (or (ask-user-for-version current-version next-versions) + (die "invalid selection."))))) + (git-tag-tree version) + (let* ((distname (format nil "~A_~A" *project-name* version)) + (tarball (format nil "~A.tar.gz" distname)) + (signature (format nil "~A.asc" tarball))) + ;; package things up. + (create-dist version distname) + (tar-and-sign distname tarball) + ;; upload. + (upload-tarball tarball signature *remote-directory*) + (update-remote-links tarball signature *host* *release-dir* *project-name*) + (when *version-file* + (upload-version-file version *version-file* *host* *version-file-dir*)) + ;; clean up. + (maybe-clean-things-up tarball signature) + ;; documentation. + (write-line "Building and uploading documentation...") + (maybe-cmd! "make -C doc upload-docs") + ;; push tags and any outstanding changes. + (write-line "Pushing tags and changes...") + (maybe-cmd! "git push --tags origin master"))) + + +;;;; Do it to it + +(let ((force nil) + (version nil) + (args ext:*args*)) + (loop while args + do (string-case (pop args) + (("-h" "--help") + (write-line "No help, sorry. Read the source.") + (ext:quit 0)) + (("-f" "--force") + (setf force t)) + (("-v" "--version") + (setf version (pop args))) + (("-n" "--dry-run") + (setf *dry-run* t)) + (t + (die "Unrecognized argument '~a'" it)))) + (run force version)) diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index 824b704059a4977f8d5df4b558f36313cdf6cf81..0000000000000000000000000000000000000000 --- a/scripts/release.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/bin/bash - -function die() { - echo -e "$@" >&2 - exit 1 -} - -if [ "$(uname -s)" = "Darwin" ]; then - die "Darwin not supported due to incompatible texinfo, sed, and expr." -fi - -### Configuration - -PROJECT_NAME='cffi' -ASDF_FILE="$PROJECT_NAME.asd" -HOST="common-lisp.net" -RELEASE_DIR="/project/$PROJECT_NAME/public_html/releases" -VERSION_FILE="VERSION" -VERSION_FILE_DIR="/project/$PROJECT_NAME/public_html" - -set -e - -### Process options - -FORCE=0 -VERSION="" - -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - echo "No help, sorry. Read the source." - exit 0 - ;; - -f|--force) - FORCE=1 - shift - ;; - -v|--version) - VERSION="$2" - shift 2 - ;; - *) - die "Unrecognized argument '$1'" - ;; - esac -done - -### Check for unrecorded changes - -if ! git diff --exit-code; then - echo -n "Unrecorded changes. " - if [ "$FORCE" -ne 1 ]; then - die "Aborting.\nUse -f or --force if you want to make a release anyway." - else - echo "Continuing anyway." - fi -fi - -### Determine new version number - -if [ -z "$VERSION" ]; then - CURRENT_VERSION=$(git tag -l v\*.\*.\* | head -n1 | cut -dv -f2) - - dots=$(echo "$CURRENT_VERSION" | tr -cd '.') - count=$(expr length "$dots" + 1) - declare -a versions - - for i in $(seq $count); do - new="" - for j in $(seq $(expr $i - 1)); do - p=$(echo "$CURRENT_VERSION" | cut -d. -f$j) - new="$new$p." - done - part=$(expr 1 + $(echo "$CURRENT_VERSION" | cut -d. -f$i)) - new="$new$part" - for j in $(seq $(expr $i + 1) $count); do new="$new.0"; done - versions[$i]=$new - done - - while true; do - echo "Current version is $CURRENT_VERSION. Which will be next one?" - for i in $(seq $count); do echo " $i) ${versions[$i]}"; done - echo -n "? " - read choice - - if ((choice > 0)) && ((choice <= ${#versions[@]})); then - VERSION=${versions[$choice]} - break - fi - done -fi - -### Do it - -DIST_NAME="${PROJECT_NAME}_${VERSION}" -TARBALL="$DIST_NAME.tar.gz" -SIGNATURE="$TARBALL.asc" - -#echo "Updating $ASDF_FILE with new version: $VERSION" -#sed -e "s/:version \"$CURRENT_VERSION\"/:version \"$VERSION\"/" \ -# "$ASDF_FILE" > "$ASDF_FILE.tmp" -#mv "$ASDF_FILE.tmp" "$ASDF_FILE" -# -#darcs record -m "update $ASDF_FILE for version $VERSION" - -echo "Tagging the tree..." -git tag "v$VERSION" - -echo "Creating distribution..." -mkdir "$DIST_NAME" -git archive master | tar xC "$DIST_NAME" - -echo "Updating $ASDF_FILE with new version: $VERSION" -sed -e "s/^(defsystem \(.*\)/(defsystem \1\n :version \"$VERSION\"/" \ - "$DIST_NAME/$ASDF_FILE" > "$DIST_NAME/$ASDF_FILE.tmp" -mv "$DIST_NAME/$ASDF_FILE.tmp" "$DIST_NAME/$ASDF_FILE" - -echo "Creating and signing tarball..." -tar czf "$TARBALL" "$DIST_NAME" -gpg -b -a "$TARBALL" - -echo "Copying tarball to web server..." -scp "$TARBALL" "$SIGNATURE" "$HOST:$RELEASE_DIR" -echo "Uploaded $TARBALL and $SIGNATURE." - -echo "Updating ${PROJECT_NAME}_latest links..." -ssh $HOST ln -sf "$TARBALL" "$RELEASE_DIR/${PROJECT_NAME}_latest.tar.gz" -ssh $HOST ln -sf "$SIGNATURE" "$RELEASE_DIR/${PROJECT_NAME}_latest.tar.gz.asc" - -if [ "$VERSION_FILE" ]; then - echo "Uploading $VERSION_FILE..." - echo -n "$VERSION" > "$VERSION_FILE" - scp "$VERSION_FILE" "$HOST":"$VERSION_FILE_DIR" - rm "$VERSION_FILE" -fi - -while true; do - echo -n "Clean local tarball and signature? [y] " - read -n 1 response - case "$response" in - y|'') - echo - rm "$TARBALL" "$SIGNATURE" - break - ;; - n) - break - ;; - *) - echo "Invalid response '$response'. Try again." - ;; - esac -done - -echo "Building and uploading documentation..." -make -C doc upload-docs - -echo "Pushing changes..." -git push --tags origin master