Skip to content
makefile-backend.lisp 13.3 KiB
Newer Older
#+xcvb
(module
  (:author ("Francois-Rene Rideau" "Stas Boukarev")
   :maintainer "Francois-Rene Rideau"
   ;; :run-depends-on ("string-escape")
   :depends-on ("profiling" "specials" "virtual-pathnames"
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
                "static-traversal" "computations" "extract-target-properties"
                "external-commands" "target-lisp-commands" "commands")))
(declaim (optimize (debug 3) (safety 3) (speed 2) (compilation-speed 0)))

(defclass makefile-traversal ()
  ())

(defclass static-makefile-traversal (static-traversal makefile-traversal)
  ())

(defvar *makefile-target-directories-to-mkdir* ())
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
(defvar *makefile-target-directories* (make-hash-table :test 'equal))
(defvar *makefile-phonies* ())
(defmethod effective-namestring ((env makefile-traversal) fullname)
  (fullname-enough-namestring env fullname))
(defmethod pseudo-effective-namestring ((env makefile-traversal) fullname)
  (pseudo-fullname-enough-namestring env fullname))

(defun computations-to-Makefile (env)
  (with-output-to-string (s)
    (dolist (computation *computations*)
      (write-computation-to-makefile env s computation))))
(defun write-makefile (fullname &key output-path)
  "Write a Makefile to output-path with information about how to compile the specified BUILD."
  (multiple-value-bind (target-dependency build directory) (handle-target fullname)
    (declare (ignore build directory))
    (let* ((env (make-instance 'static-makefile-traversal))
           (default-output-path (subpathname *workspace* "xcvb.mk"))
                (merge-pathnames* output-path default-output-path)
           (makefile-path (ensure-pathname-absolute actual-output-path))
           (makefile-dir (pathname-directory-pathname makefile-path))
           (*default-pathname-defaults* makefile-dir)
           (*print-pretty* nil); otherwise SBCL will slow us down a lot.
           (*makefile-target-directories* (make-hash-table :test 'equal))
           (*makefile-target-directories-to-mkdir* nil)
           (lisp-env-var (lisp-environment-variable-name :prefix nil))
           (*lisp-executable-pathname* ;; magic escape!
            (list :makefile "${" lisp-env-var "}")))
      (log-format 9 "output-path: ~S" output-path)
      (log-format 9 "default-output-path: ~S" default-output-path)
      (log-format 9 "actual-output-path: ~S" actual-output-path)
      (log-format 6 "makefile-path: ~S" makefile-path)
      (log-format 9 "*default-pathname-defaults*: ~S" *default-pathname-defaults*)
      (log-format 7 "workspace: ~S" *workspace*)
      (log-format 7 "cache: ~S" *cache*)
      (log-format 7 "object-cache: ~S" *object-cache*)
      ;; Pass 1: Traverse the graph of dependencies
      (log-format 8 "T=~A building dependency graph" (get-universal-time))
      (graph-for env target-dependency)
      ;; Pass 2: Build a Makefile out of the *computations*
      (log-format 8 "T=~A computing makefile body" (get-universal-time))
      (log-format 8 "All *computations*=~%~S" (reverse *computations*))
      (let ((body (computations-to-Makefile env)))
        (log-format 8 "T=~A creating makefile" (get-universal-time))
        (ensure-directories-exist makefile-path)
        (with-open-file (out makefile-path
                             :direction :output
                             :if-exists :supersede)
          (log-format 8 "T=~A printing makefile" (get-universal-time))
          (write-makefile-prelude
           :stream out :lisp-env-var lisp-env-var)
          (princ body out)
          (write-makefile-conclusion out)))
      (log-format 8 "T=~A done" (get-universal-time))
      ;; Return data for use by the non-enforcing Makefile backend.
      (values makefile-path makefile-dir))))
(defparameter +generated-file-warning-start+
  "### This file was automatically created by XCVB")

(defun write-generated-file-warning (stream implementation-pathname)
  (format stream "~
~A ~A with the arguments~%~
### ~{~A~^ ~}~%~
### It may have been specialized to the target implementation ~A~%~
### from ~A with the following features:~%~
### DO NOT EDIT! Changes will be lost when XCVB overwrites this file.~%~%"
          +generated-file-warning-start+
          *xcvb-version* *arguments* *lisp-implementation-type*
          implementation-pathname *target-system-features*))

(defun write-makefile-prelude (&key stream lisp-env-var)
  (let ((vars (list lisp-env-var))
        (implementation-pathname
         (or *target-lisp-executable-pathname*
             (lisp-implementation-name (get-lisp-implementation)))))
    (write-generated-file-warning stream implementation-pathname)
    (format stream "X~A ?= ~A~%~2:*~A ?= ${X~:*~A}~%" lisp-env-var implementation-pathname)
    (case *lisp-implementation-type*
      ((:ccl :sbcl)
       (let ((dir-var (lisp-implementation-directory-variable (get-lisp-implementation))))
         (format stream "_o_ = ~A~%~A ?= $(shell $(_o_))~%"
                 (escape-string-hashes
                  (shell-tokens-to-Makefile
                   (lisp-invocation-arglist
                    :cross-compile nil
                    :eval (format nil "(progn (princ ~A)(terpri)~A)"
                                  (association '*lisp-implementation-directory*
                                               *target-properties-variables*)
                                  (quit-form)))))
                 dir-var)
         (append1f vars dir-var))))
    (format stream "export~{ ~A~}~%~%" vars))
  (format stream "
  XCVB_EOD := xcvb-ensure-object-directories
endif~2%"
          (join-strings
           (mapcar #'escape-string-for-Makefile
                   (mapcar 'enough-namestring
                           *makefile-target-directories-to-mkdir*))
           :separator " ")))
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
;; * a clean-xcvb target that removes the object directory
(defun write-makefile-conclusion (&optional stream)
  (format stream "
xcvb-ensure-object-directories:
.PHONY: force xcvb-ensure-object-directories~{ ~A~}~2%"
          (shell-tokens-to-Makefile
           (mapcar 'enough-namestring *makefile-target-directories-to-mkdir*))
          *makefile-phonies*))
(defun ensure-makefile-will-make-pathname (env namestring)
  (declare (ignore env))
  (let* ((p (position #\/ namestring :from-end t :end nil))
         (dir (subseq namestring 0 p)))
    (unless (gethash dir *makefile-target-directories*)
      (setf (gethash dir *makefile-target-directories*) t)
      (unless (find-if (lambda (d) (portable-namestring-prefix<= dir d))
                       *makefile-target-directories-to-mkdir*)
        (setf *makefile-target-directories-to-mkdir*
              (cons dir
                    (remove-if (lambda (d) (portable-namestring-prefix<= d dir))
                               *makefile-target-directories-to-mkdir*))))))
(defmethod vp-namestring :around ((env makefile-traversal) vp)
  (let ((namestring (call-next-method)))
    (when (eq (vp-root vp) :obj)
      (ensure-makefile-will-make-pathname env namestring))
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
    namestring))
(defmethod grain-pathname-text ((env makefile-traversal) (grain file-grain))
  (let ((pathname (call-next-method)))
    (values (escape-sh-token-for-Makefile (enough-namestring pathname)) pathname)))

(defmethod grain-pathname-text :around ((env makefile-traversal) grain)
  (declare (ignorable env grain))
  (or (call-next-method) ""))

(defun Makefile-commands-for-computation (env computation-command)
  (mapcar 'shell-tokens-to-Makefile
          (external-commands-for-computation env computation-command)))
(defun write-computation-to-makefile (env stream computation)
  (with-accessors ((command computation-command)
                   (inputs computation-inputs)
                   (outputs computation-outputs)) computation
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
    (let* ((first-output (first outputs))
           (dependencies (mapcar #'grain-computation-target inputs))
           (target (grain-pathname-text env first-output))
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
           (other-outputs (rest outputs)))
      (dolist (o other-outputs)
        (format stream "~&~A: ~A~%" (grain-pathname-text env o) target))
      (format stream "~&~A:~{~@[ ~A~]~}~@[~A~] ${XCVB_EOD}~%"
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
              target
              (mapcar/ #'grain-pathname-text env dependencies)
              (asdf-dependency-text env first-output dependencies))
      (when command
                    (Makefile-commands-for-computation env command)))
          (format stream "~C@~A~%" #\Tab c)))
      (terpri stream))))
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
;;; This is only done for images, not for individual files.
;;; For finer granularity, we could possibly define for each ASDF system
;;; (and implementation) a variable
;;; ASDF_CL_PPCRE_UP_TO_DATE := $(shell ...)
;;; but that would require more work.
;;; Also, it doesn't make sense to try to beat ASDF at its own game:
;;; if you really want proper dependencies,
;;; you'll migrate from ASDF to XCVB anyway.
(defun asdf-dependency-text (env output inputs)
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
  (with-nesting ()
    (when (image-grain-p output))
    (let ((asdf-grains (remove-if-not #'asdf-grain-p inputs))))
    (when asdf-grains)
    (let* ((image-namestring (grain-namestring env output))
           (pathname-text (escape-sh-token-for-Makefile
                           (enough-namestring image-namestring)))))
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
    (with-output-to-string (s)
      (format s " $(shell [ -f ~A ] && " pathname-text)
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
      (shell-tokens-to-Makefile
       (lisp-invocation-arglist
        :eval (format nil "(xcvb-driver::asdf-systems-up-to-date~{ ~S~})"
                      (mapcar #'asdf-grain-system-name asdf-grains)))
Francois-Rene Rideau's avatar
Francois-Rene Rideau committed
       s)
      (format s " || echo force)"))))
(defmethod grain-pathname-text ((env makefile-traversal) (grain phony-grain))
  (declare (ignore env))
  (let ((n (normalize-name-for-makefile (princ-to-string (fullname grain)))))
    (pushnew n *makefile-phonies* :test 'equal)
    n))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Make-Makefile ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define-command make-makefile
    (("make-makefile" "mkmk" "mm")
     (&rest keys &key)
     `(,@+build-option-spec+
       ,@+setup-option-spec+
       ,@+base-image-option-spec+
       ,@+source-registry-option-spec+
       (("output-path" #\o) :type string :initial-value "xcvb.mk" :documentation "specify output path")
       ,@+workspace-option-spec+
       ,@+install-option-spec+
       ,@+lisp-implementation-option-spec+
       ,@+cfasl-option-spec+
       (("master" #\m) :type boolean :optional t :initial-value t :documentation "enable XCVB-master")
       ,@+verbosity-option-spec+
       ,@+profiling-option-spec+)
     "Create some Makefile"
     "Create Makefile rules to build a project." ignore)
  (apply 'make-build :makefile-only t keys))


(defun read-integer (x)
  (parse-integer x :junk-allowed t))

(defun slurp-stream-integer (input-stream)
  (read-integer (slurp-stream-string input-stream)))

(defmethod slurp-input-stream ((x (eql :integer)) input-stream
                               &key &allow-other-keys)
  (slurp-stream-integer input-stream))

  (ignore-errors
    (cond
      ((featurep :linux)
       (run-program/ '("grep" "-c" "^processor " "/proc/cpuinfo") :output :integer))
      ((featurep :darwin)
       (run-program/ '("sysctl" "-n" "hw.ncpu") :output :integer))
      ((os-windows-p)
       (read-integer (getenv "NUMBER_OF_PROCESSORS"))))))

(defun make-parallel-flag ()
  (if-bind (ncpus) (ncpus)
    (format nil "-l~A" (1+ ncpus))
    "-j"))

(defun invoke-make (&key target directory makefile parallel ignore-error-status env)
  (let* ((make (or (getenv "MAKE") "make"))
         (make-command
          `(,@(when env `("env" ,@env))
            ,make
            ,@(when parallel (list (make-parallel-flag)))
            ,@(when directory `("-C" ,(namestring directory)))
            ,@(when makefile `("-f" ,(namestring makefile)))
            ,@(when target (ensure-list target)))))
      (log-format 6 "Building with ~S" make-command)
      (run-program/ ;; for side-effects
       make-command ; (strcat (escape-shell-command make-command) " >&2")
       :ignore-error-status ignore-error-status)))

(define-command make-build
    (("make-build" "mkb" "mb")
     (&rest keys &key makefile-only (retry t) (exit t))
     `(,@+make-makefile-option-spec+
       (("parallel" #\j) :type boolean :optional t :initial-value t :documentation "build in parallel"))
     "Use Make to build your project (in parallel)"
     "Create Makefile rules to build a project, use them."
     (build output-path parallel))
  (apply 'handle-global-options keys)
  (with-maybe-profiling ()
    (multiple-value-bind (makefile-path makefile-dir)
        (write-makefile build :output-path output-path)
      (if makefile-only
          (values makefile-path makefile-dir)
          (let ((code (invoke-make
                       :directory makefile-dir :makefile makefile-path :parallel parallel
                       :ignore-error-status t)))
            (unless (zerop code)
              (when retry
                (invoke-make
                 :directory makefile-dir :makefile makefile-path :parallel parallel
                 :ignore-error-status t :env '("XCVB_DEBUGGING=t"))))
            (if exit
                (exit code)
                (values code makefile-dir makefile-path)))))))