;;;; -*- Mode:Common-Lisp; Package:MODULE-MANAGER; Syntax:common-lisp -*- ;;;; *-* File: /usr/local/gbbopen/source/module-manager/module-manager.lisp *-* ;;;; *-* Edited-By: cork *-* ;;;; *-* Last-Edit: Wed May 6 15:01:36 2015 *-* ;;;; *-* Machine: phoenix.corkills.org *-* ;;;; ************************************************************************** ;;;; ************************************************************************** ;;;; * ;;;; * Module Manager Facility ;;;; * ;;;; ************************************************************************** ;;;; ************************************************************************** ;;; ;;; Written by: Dan Corkill (incorporating some original ideas by ;;; Kevin Gallagher and Zachary Rubinstein) ;;; ;;; Copyright (C) 2002-2016, Dan Corkill ;;; Part of the GBBopen Project. ;;; Licensed under Apache License 2.0 (see LICENSE for license information). ;;; ;;; Porting Notice: ;;; ;;; The directory probing function, probe-directory, that is defined in ;;; this file must be extended when porting to a new CL implementation. ;;; ;;; -------------------------------------------------------------------------- ;;; ;;; This Module Manager Facility provides a lightweight and easy to use ;;; mechanism for maintaining (compiling and loading) module files. ;;; ;;; This file assumes the global variables *compiled-directory-name* and ;;; *compiled-file-type* have been defined by loading ;;; module-manager-loader.lisp. ;;; ;;; The Module Manager Facility supports the following directory layout: ;;; ;;; ;;; / \ ;;; / \ ;;; source ... ;;; / \ / \ ;;; / .. .. \ ;;; module-manager module-manager ;;; / \ ;;; / \ ;;; module-manager.lisp module-manager. ;;; ;;; This file can be used as a stand-alone system (when loaded by its ;;; companion file, module-manager-loader.lisp). Instructions for stand-alone ;;; usage of the Module Manager Facility are provided in the ;;; module-manager-startup.lisp file. ;;; ;;; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ;;; ;;; 07-13-02 File created. (Corkill) ;;; 01-12-04 Added :create-dirs option to compile-module. (Corkill) ;;; 01-29-04 Exported MODULE-LOADED-P. (Corkill) ;;; 02-01-04 Support use of existing root-directory in DEFINE-ROOT-DIRECTORY. ;;; (Corkill) ;;; 03-19-04 Added top-level Module Manager commands for Lispworks. (Corkill) ;;; 03-19-04 Added file-options checking. (Corkill) ;;; 06-10-04 Added proper :forces-recompile date checking and warning ;;; messages. (Corkill) ;;; 06-11-04 Moved to separate package (for stand-alone use). (Corkill) ;;; 08-10-04 Removed MAKE-DIRECTORY in favor of ENSURE-DIRECTORIES-EXIST. ;;; (Corkill) ;;; 08-18-04 Add missing slot-definition documentation method for Digitool ;;; MCL. (Corkill) ;;; 02-06-05 Added LOAD-MODULE-FILE. (Corkill) ;;; 02-08-05 Added DESCRIBE-MODULE and BRIEF-DATE-AND-TIME. (Corkill) ;;; 05-22-05 Added ECL support. (Corkill) ;;; 06-08-05 Added CLISP support. (sds) ;;; 06-18-05 Added MODULE-DIRECTORIES. (Corkill) ;;; 02-13-06 Added GCL support. (Corkill) ;;; 04-11-06 Added *load-pathname* relative :directory option to ;;; DEFINE-MODULE. (Corkill) ;;; 05-08-06 Added support for the Scieneer CL. (dtc) ;;; 11-21-06 Added GET-DIRECTORY. (Corkill) ;;; 06-06-07 Added :after-form support for modules (somewhat reluctantly, ;;; as putting forms in a module's files is preferable to having ;;; them in the module definition). (Corkill) ;;; 07-14-07 Added subdirectories support to DEFINE-ROOT-DIRECTORY. (Corkill) ;;; 07-14-07 Added :noautorun compile/load-module option. (Corkill) ;;; 12-19-07 Added module-relative support to COMPUTE-RELATIVE-DIRECTORY and ;;; incremented Module Manager version to 1.2. (Corkill) ;;; 01-05-08 Skip undefined modules when performing compatiblity-ordering ;;; check of a module. (Corkill) ;;; 03-29-08 Added :nopropagate (:propagate canceling) compile/load-module ;;; option. (Corkill) ;;; 04-16-08 Support "Source" and "SOURCE" directory-name conventions (in ;;; addition to conventional "source"). (Corkill) ;;; 04-19-08 Added application-version-identifier support to ;;; DEFINE-ROOT-DIRECTORY and incremented version to 1.3. (Corkill) ;;; 05-15-08 Added PARSE-DATE. (Corkill) ;;; 06-23-08 Added BRIEF-DATE. (Corkill) ;;; 03-06-09 Added ending bounding-index second return value to PARSE-DATE. ;;; (Corkill) ;;; 09-26-16 Allow directory redefinition in ENSURE-MODULE (per Zack). ;;; (Corkill) ;;; ;;; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * (eval-when (:compile-toplevel :load-toplevel :execute) (unless (find-package ':module-manager) (error "This file should be loaded using the file ~ module-manager-loader.lisp"))) (in-package :module-manager) (eval-when (:compile-toplevel :load-toplevel :execute) (import '(#+xcl extensions:probe-directory))) ;;; --------------------------------------------------------------------------- ;;; Check if we are good to go: (flet ((check-var (var) (unless (boundp var) (error "~s is not defined.~ (This file should be loaded using the file ~ module-manager-loader.lisp)" var)))) (check-var '*compiled-directory-name*) (check-var '*compiled-file-type*)) ;;; =========================================================================== ;;; Allow-redefinition (placed here for very early use) (eval-when (:compile-toplevel :load-toplevel :execute) (defmacro allow-redefinition (&body body) ;;; Still need to support CMUCL, ECL, SBCL, and SCL `(#+allegro excl:without-redefinition-warnings #+clisp let #+clisp ((custom:*suppress-check-redefinition* 't)) #+clozure let #+clozure ((ccl:*warn-if-redefine* nil)) #+digitool-mcl let #+digitool-mcl ((ccl:*warn-if-redefine* nil)) #+lispworks system::without-warning-on-redefinition #-(or allegro clisp clozure digitool-mcl lispworks) progn (progn ,@body)))) ;;; =========================================================================== ;;; Imports to support using extended REPL commands: (eval-when (:compile-toplevel :load-toplevel :execute) (import '(common-lisp-user::*current-system-name* common-lisp-user::define-repl-command common-lisp-user::with-system-name))) (declaim (special *current-system-name*)) (unless (boundp '*current-system-name*) (setf *current-system-name* nil)) (unless (macro-function 'with-system-name) ;; This is a copy of the definition in ../../extended-repl.lisp. It is ;; needed to support startup.lisp only invocation. (allow-redefinition (defmacro with-system-name ((&optional system-name) &body body) (unless (keywordp system-name) (error "System name, ~s, must be a keyword." system-name)) `(let ((*current-system-name* ',system-name)) ,@body)))) ;;; =========================================================================== ;;; CL-User Global Variables ;;; ;;; Some CL implementations generate redefinition warnings when performing a ;;; compile/load/compile bootstrap sequence, so we don't use defvar's here to ;;; set default values. (eval-when (:compile-toplevel :load-toplevel :execute) (import '(common-lisp-user::*automatically-create-missing-directories* common-lisp-user::*autorun-modules* common-lisp-user::*module-manager-compile-verbose* common-lisp-user::*module-manager-load-verbose* common-lisp-user::*patches-only*))) ;;; --------------------------------------------------------------------------- ;;; Controls whether the Module Manager Facility automatically creates missing ;;; directories (without asking the user): (declaim (special *automatically-create-missing-directories*)) (unless (boundp '*automatically-create-missing-directories*) (setf *automatically-create-missing-directories* 't)) ;;; --------------------------------------------------------------------------- ;;; Controls whether the Module Manager Facility compiles/loads patches only: (declaim (special *patches-only*)) (unless (boundp '*patches-only*) (setf *patches-only* nil)) ;;; --------------------------------------------------------------------------- ;;; When true, the Module Manager Facility will generate its own compile & ;;; load messages if the corresponding *compile-verbose* or *load-verbose* ;;; values are nil. (declaim (special *module-manager-compile-verbose*)) (unless (boundp '*module-manager-compile-verbose*) (setf *module-manager-compile-verbose* nil)) (declaim (special *module-manager-load-verbose*)) (unless (boundp '*module-manager-load-verbose*) (setf *module-manager-load-verbose* nil)) ;;; --------------------------------------------------------------------------- ;;; Controls whether modules (such as GBBopen example and tests) autorun ;;; themselves. (declaim (special *autorun-modules*)) (unless (boundp '*autorun-modules*) (setf *autorun-modules* 't)) ;;; =========================================================================== ;;; Implementation-Specific Package & Feature Adjustments ;; Allow use of CMUCL package nicknames with SBCL: #+sbcl (eval-when (:compile-toplevel :load-toplevel :execute) (let ((fn (or ;; Pre SBCL-1.0.34: (find-symbol "ENTER-NEW-NICKNAMES" :sb-impl) ;; Post SBCL-1.0.34: (find-symbol "%ENTER-NEW-NICKNAMES" :sb-impl)))) (funcall fn (find-package "SB-PCL") '("PCL")) (funcall fn (find-package "SB-UNIX") '("UNIX")))) ;;; =========================================================================== ;;; Export user-level Module Manager names. (Some of these names could ;;; collide with similar names in other packages, but we export them anyway.) (eval-when (:compile-toplevel :load-toplevel :execute) (export '(*automatically-create-missing-directories* ; re-exported from ; :cl-user *autorun-modules* ; re-exported from :cl-user *current-module* ; not documented *current-system-name* ; re-exported from :cl-user *module-manager-compile-verbose* ; not yet documented *module-manager-load-verbose* ; not yet documented *month-precedes-date* ; part of tools, but placed here *patches-only* ; re-exported from :cl-user allow-redefinition ; part of tools, but placed here brief-date ; part of tools, but placed here brief-date-and-time ; part of tools, but placed here check-all-module-requires-orderings ; not yet documented compile-module compute-relative-directory ; not documented continue-patch define-relative-directory define-root-directory define-repl-command ; re-exported from :cl-user define-module describe-module describe-patches dotted-conc-name ; part of tools, but placed here; not ; documented finish-patch feature-present-p ; part of tools, but placed here; not ; documented freeze-module ; not yet documented get-directory get-patch-description get-root-directory list-modules ; not yet documented load-module load-module-file module ; documentation doc-type name module-manager-implementation-version ; not documented module-directories ; not yet documented module-loaded-p parse-date ; part of tools, but placed here patch patch-loaded-p printv ; part of tools, but placed here printv-expander ; part of tools, but placed here (not ; documented) probe-directory ; not yet documented show-defined-directories show-modules ; not yet documented start-patch undefine-directory ; not yet documented undefine-module ; not yet documented unfreeze-module ; not yet documented with-system-name ; re-exported from :cl-user with-module-redefinitions ; not yet documented ))) ;;; =========================================================================== (allow-redefinition (defun module-manager-implementation-version () "1.6")) ;;; Added to *features* at the end of this file: (defparameter *module-manager-version-keyword* ;; Support cross-case mode CLs: (read-from-string (format nil ":module-manager-~a" (module-manager-implementation-version)))) ;;; --------------------------------------------------------------------------- (allow-redefinition (defun print-module-manager-herald () (format t "~%;;; ~72,,,'-<-~> ;;; Module-Manager System ~a ;;; ;;; Developed and supported by the GBBopen Project (http:/GBBopen.org/) ;;; (See http://GBBopen.org/downloads/LICENSE for license details.) ;;; ~72,,,'-<-~>~2%" (module-manager-implementation-version))) (eval-when (:load-toplevel) (print-module-manager-herald))) ;;; =========================================================================== ;;; Add missing slot-definition documentation method to Digitool MCL: #+digitool-mcl (defmethod documentation ((object ccl::standard-slot-definition) doc-type) (declare (ignore doc-type)) (when (and (slot-exists-p object 'documentation) (slot-boundp object 'documentation)) (slot-value object 'documentation))) ;;; =========================================================================== ;;; Printv ;;; ;;; A handy debugging macro ;;; ;;; Placed here to make this macro available ASAP ;;; ;;; --------------------------------------------------------------------------- ;;; NOTE: Copy any changes to these PRINTV definitions to the stand-alone ;;; ../../printv.lisp file ;;; --------------------------------------------------------------------------- (defun printv-separator () (format *trace-output* "~&;; ~60,,,'-<-~>~%") (force-output *trace-output*)) ;;; --------------------------------------------------------------------------- (defun printv-form-printer (form) (typecase form ;; String (label): (string (format *trace-output* "~&;; ~a~%" form)) ;; Evaluated form: ((or cons (and symbol (not keyword))) (format *trace-output* "~&;; ~w =>" form)) ;; Self-evaluating form: (t (format *trace-output* "~&;; ~s~%" form))) (force-output *trace-output*)) ;;; --------------------------------------------------------------------------- (defun printv-values-printer (values-list) (format *trace-output* "~:[ [returned 0 values]~;~:*~{ ~w~^;~}~]~%" values-list) (force-output *trace-output*)) ;;; --------------------------------------------------------------------------- (defun printv-expander (forms ;; Allow for customized printv-style printv'ers: &optional values-trans-fn) (let ((result-sym (gensym))) `(let ((*print-readably* nil) ,result-sym) ,@(loop for form in forms nconcing (cond ;; Separator requested? ((eq form ':hr) ;; list used for splicing protection... (list '(printv-separator))) ;; Evaluated form: ((or (consp form) (and (symbolp form) (not (keywordp form)))) `((printv-form-printer ',form) (printv-values-printer ,(if values-trans-fn `(funcall ,values-trans-fn (setf ,result-sym (multiple-value-list ,form))) `(setf ,result-sym (multiple-value-list ,form)))))) ;; Self-evaluating form: (t `((printv-form-printer (car (setf ,result-sym (list ,form)))))))) (values-list ,result-sym)))) ;;; --------------------------------------------------------------------------- (eval-when (:compile-toplevel :load-toplevel :execute) (defmacro printv (&rest forms) (printv-expander forms))) ;;; =========================================================================== ;;; Feature-present-p and dotted-conc-name (eval-when (:compile-toplevel :load-toplevel :execute) (defun feature-present-p (feature-symbol) ;; Checks if :full-safety is on the *features* list (used at execution ;; time to conditionalize generated code, in place of read-time ;; conditionals) (member (symbol-name feature-symbol) *features* :test #'string=))) ;;; --------------------------------------------------------------------------- (defun dotted-conc-name (symbol) ;; Support reader-case-preserving CLs (concatenate 'simple-string (symbol-name symbol) ".")) ;;; =========================================================================== ;;; WITH-FULL-OPTIMIZATION for use in this file (a copy of the definition in ;;; ../tools/declarations.lisp) (eval-when (:compile-toplevel :load-toplevel :execute) (defmacro with-full-optimization ((&key) &body body) ;; The feature :full-safety disables with-full-optimization optimizations: `(locally #+full-safety () #-full-safety (declare (optimize (speed 3) (safety 0) (space 0) (debug 0) (compilation-speed 0) #+lispworks (system:interruptable 0))) ,@body))) ;;; =========================================================================== ;;; Basic declared numerics for use in this file (eval-when (:compile-toplevel :load-toplevel :execute) (defmacro & (arg) ;;; Wraps (the fixnum ...) around `arg' (if (feature-present-p ':full-safety) `,arg `(the fixnum ,arg))) (defmacro +& (&rest args) `(& (+ ,.(mapcar #'(lambda (x) `(& ,x)) args)))) (defmacro -& (&rest args) `(& (- ,.(mapcar #'(lambda (x) `(& ,x)) args)))) (defmacro <=& (&rest args) `(<= ,.(mapcar #'(lambda (x) `(& ,x)) args))) (defmacro >& (&rest args) `(> ,.(mapcar #'(lambda (x) `(& ,x)) args))) (define-modify-macro incf& (&optional (increment 1)) +&)) ;;; =========================================================================== ;;; BRIEF-DATE, BRIEF-DATE-AND-TIME, and PARSE-DATE. These entities are ;;; really part of the GBBopen-tools module, but they are placed here to ;;; allow stand-alone use with the :module-manager package. PARSE-DATE is ;;; particularly complex, but date parsing is used in conjunction with ;;; patches. (defvar *month-precedes-date* 't) (defparameter *month-name-vector* #("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")) (defparameter *month-full-name-vector* #("January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December")) (defparameter *weekday-abbreviation-vector* #("Mo" "Tu" "We" "Th" "Fr" "Sa" "Su")) (defparameter *weekday-name-vector* #("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun")) (defparameter *weekday-full-name-vector* #("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday")) (declaim (type simple-vector *month-name-vector* *month-full-name-vector* *weekday-abbreviation-vector* *weekday-name-vector* *weekday-full-name-vector*)) ;;; --------------------------------------------------------------------------- (defun junk-in-string-error (string) (error "There's junk in this string: ~s" string)) ;;; --------------------------------------------------------------------------- (defun decode-supplied-universal-time (universal-time time-zone) (cond ((not universal-time) (setf universal-time (get-universal-time))) ;; User likely forgot the optional `universal-time' value: ((keywordp universal-time) (error "The optional universal-time value must be supplied when ~ specifying keyword options."))) (if time-zone (decode-universal-time universal-time time-zone) (decode-universal-time universal-time))) ;;; --------------------------------------------------------------------------- (locally ;; SBCL (rightly) complains about combining &optional and &key, but we ;; ignore that here: #+sbcl (declare (sb-ext:muffle-conditions style-warning)) (defun brief-date (&optional universal-time &key (month-precedes-date *month-precedes-date*) year-first (include-year 't) time-zone destination) ;;; Returns formatted date string (multiple-value-bind (second minute hour date month year) (decode-supplied-universal-time universal-time time-zone) (declare (ignore second minute hour) (fixnum month)) (unless include-year (setf year nil)) (let ((month-name (with-full-optimization () (svref (the (simple-array t (*)) *month-name-vector*) (& (1- month)))))) (if (and year-first year) (if month-precedes-date (format destination "~s, ~a ~2d" year month-name date) (format destination "~s, ~2d ~a" year date month-name)) (if month-precedes-date (format destination "~a ~2d~@[, ~s~]" month-name date year) (format destination "~2d ~a~@[, ~s~]" date month-name year))))))) ;;; --------------------------------------------------------------------------- (locally ;; SBCL (rightly) complains about combining &optional and &key, but we ;; ignore that here: #+sbcl (declare (sb-ext:muffle-conditions style-warning)) (defun brief-date-and-time (&optional universal-time &key (month-precedes-date *month-precedes-date*) year-first time-zone include-seconds destination) ;;; Returns formatted date/time string (brief, Unix ls-like form) (let ((current-time (get-universal-time)) time-difference) (if universal-time (setf time-difference (abs (- current-time universal-time))) (setf universal-time current-time time-difference 0)) (multiple-value-bind (second minute hour date month year) (decode-supplied-universal-time universal-time time-zone) (declare (fixnum month)) (let ((month-name (with-full-optimization () (svref (the (simple-array t (*)) *month-name-vector*) (& (1- month)))))) (if (with-full-optimization () (< (& time-difference) ;; 120 days: #.(* 60 60 24 120))) (if month-precedes-date (format destination "~a ~2d ~2,'0d:~2,'0d~:[~;:~2,'0d~]" month-name date hour minute include-seconds second) (format destination "~2d ~a ~2,'0d:~2,'0d~:[~;:~2,'0d~]" date month-name hour minute include-seconds second)) (if year-first (if month-precedes-date (format destination "~s, ~a ~2d~@[ ~]" year month-name date include-seconds) (format destination "~s, ~2d ~a~@[ ~]" year date month-name include-seconds)) (if month-precedes-date (format destination "~a ~2d, ~s~@[ ~]" month-name date year include-seconds) (format destination "~2d ~a, ~s~@[ ~]" date month-name year include-seconds))))))))) ;;; --------------------------------------------------------------------------- ;;; Helper functions for PARSE-DATE (defun day-of-week (date month year) (nth-value 6 (decode-universal-time (encode-universal-time 0 0 0 date month year)))) #-full-safety (define-compiler-macro day-of-week (date month year) `(nth-value 6 (decode-universal-time (encode-universal-time 0 0 0 ,date ,month ,year)))) ;;; --------------------------------------------------------------------------- (defun 1st-day-of-month (month year) ;; Returns the day of the week (0: Monday ... 6: Sunday) of the 1st of ;; `month' in `year': (day-of-week 1 month year)) #-full-safety (define-compiler-macro 1st-day-of-month (month year) `(day-of-week 1 ,month ,year)) ;;; --------------------------------------------------------------------------- (with-full-optimization () (defun leap-year-p (year) (declare (fixnum year)) (or (and (zerop (& (mod year 4))) (not (zerop (& (mod year 100))))) (zerop (& (mod year 400)))))) ;;; --------------------------------------------------------------------------- (with-full-optimization () (defun last-date-of-month (month year) ;; Returns the date of the last day in `month' of `year' (declare (fixnum month)) (let ((last-day ;; non-leap-year month lengths: (svref #(31 28 31 30 31 30 31 31 30 31 30 31) (& (1- month))))) ;; Adjust February, if `year' is a leap year: (if (and (= month 2) (leap-year-p year) ) (& (1+ (& last-day))) last-day)))) ;;; --------------------------------------------------------------------------- (with-full-optimization () (defun last-date-of-day-in-month (day month year) ;; Returns the date of the last weekday `day' in `month' of `year' (declare (fixnum day)) (let* ((last-date-of-month (last-date-of-month month year)) (last-day-of-month (day-of-week last-date-of-month month year))) (declare (fixnum last-date-of-month last-day-of-month)) (let ((result (& (+ last-date-of-month (& (- day last-day-of-month)))))) (if (< last-day-of-month day) (& (- result 7)) result))))) ;;; --------------------------------------------------------------------------- (with-full-optimization () (defun convert-descriptive-date (descriptive-date month year) (declare (simple-string descriptive-date)) (cond ;; lastDAY: ((and (= (& (length descriptive-date)) 7) (string-equal descriptive-date "last" :end1 4)) (flet ((day-equal (day) (string-equal descriptive-date day :start1 4))) (declare (dynamic-extent #'day-equal)) (let ((pos (position-if #'day-equal *weekday-name-vector*))) (when pos (last-date-of-day-in-month pos month year))))) ;; DAY>=N (or DAY>=NN): ((string-equal descriptive-date ">=" :start1 3 :end1 5) (flet ((day-equal (day) (string-equal descriptive-date day :end1 3))) (declare (dynamic-extent #'day-equal)) (let ((pos (position-if #'day-equal *weekday-name-vector*))) (when pos (let* ((specified-date (parse-integer descriptive-date :start 5)) (day-of-specified-date (day-of-week specified-date month year)) (date-increment (-& pos day-of-specified-date))) (+& specified-date (if (minusp (& date-increment)) (+& 7 date-increment) date-increment))))))) ;; Shouldn't happen (as the descriptive-date parser should catch ;; problems): (t (error "Unknown descriptive date: ~s" descriptive-date))))) ;;; --------------------------------------------------------------------------- (defun parse-date (string &key (start 0) (end (length string)) (junk-allowed nil) (separators "-/ ,") (month-precedes-date *month-precedes-date*) year-first default-to-current-year) ;;; Parses many intuitive date formats (sensitive to month-precedes-date, ;;; if needed): (declare (simple-string string) (fixnum end)) ;; Ensure that `separators' is a simple string: (unless (typep separators 'simple-string) (setf separators (coerce separators 'simple-string))) (with-full-optimization () (let ((ptr start) ;; result values year month date ;; holds undecided numeric fields: 1st-numeric 2nd-numeric 3rd-numeric ;; holds descriptive-date string until month and year can be ;; determined: descriptive-date ;; for ugly internal result assignments: name-equal-string result ;; holds cached GET-DECODED-TIME values, should they be needed ;; again: current-date current-month current-year ;; additional state holders: 1st-field-is-month?) (declare (fixnum ptr)) (labels ((at-separator-char-p (&optional (ptr ptr)) (declare (fixnum ptr)) (find (schar string ptr) (the simple-string separators))) (skip-separators () (loop while (and (< ptr end) (at-separator-char-p)) do (incf& ptr))) (safe-string-equal (name) (let ((end2 (+& ptr (length name)))) (declare (fixnum end2)) (when (>= end end2) (string-equal name string :start2 ptr :end2 end2)))) (saving-name-equal (name) (declare (simple-string name)) (when (safe-string-equal name) (setf name-equal-string name))) ;; Skips full or abbreviated weekday names: (process-possible-day () (setf name-equal-string nil) (when (or (position-if #'saving-name-equal *weekday-full-name-vector*) (position-if #'saving-name-equal *weekday-name-vector*) (position-if #'saving-name-equal *weekday-abbreviation-vector*)) (let ((new-ptr (+& ptr (length (the simple-string name-equal-string))))) (when (at-separator-char-p new-ptr) (setf ptr new-ptr) (skip-separators))))) ;; Process numeric field: (process-field-if-numeric () (when (< ptr end) (unless (alpha-char-p (schar string ptr))) (multiple-value-setq (result ptr) (parse-integer string :start ptr :end end :junk-allowed t)) (when result (skip-separators) result))) ;; Sets the current decoded time values, but only once: (get-decoded-time-unless-cached () ;; check current-date to see if we've cached already: (unless current-date (multiple-value-bind (seconds minutes hours date month year) (get-decoded-time) (declare (ignore seconds minutes hours)) (setf current-date date current-month month current-year year)))) ;; Used when a year is not specified: (use-assumed-year () (get-decoded-time-unless-cached) (setf year current-year) (unless default-to-current-year ;; Assume next year, if the date is past in the current year: (when (or (< (& month) (& current-month)) (and (= (& month) (& current-month)) (< (& (or date 1)) ; date is 1 if not specified (& current-date)))) (incf& year)))) ;; Upgrade year YY to YYYY -- YY assumed within +/- 50 years from ;; current time (if year < 100): (maybe-upgrade-year () (unless (>= (& year) 100) (get-decoded-time-unless-cached) (let ((current-century (& (* 100 (& (truncate (& current-year) 100)))))) (setf year (if (>= (& year) 50) (+& year current-century -100) (+& year current-century)))))) (process-month-if-alpha () (setf name-equal-string nil) (let ((pos (or (position-if #'saving-name-equal *month-full-name-vector*) (position-if #'saving-name-equal *month-name-vector*)))) (when pos (let ((maybe-new-ptr (+ ptr (length (the simple-string name-equal-string))))) (when (or (= maybe-new-ptr end) (at-separator-char-p maybe-new-ptr)) (setf ptr maybe-new-ptr) (skip-separators) (setf month (1+ (& pos)))))))) (process-date-if-alpha () (let ((end-ptr (+ ptr 6))) (declare (fixnum end-ptr)) (when (and (<= end-ptr end) (or ;; DAY>=N (or DAY>=NN): (let ((end2 (+ ptr 3))) (declare (fixnum end2)) (and (flet ((test-it (name) (string-equal name string :start2 ptr :end2 end2))) (declare (dynamic-extent #'test-it)) (find-if #'test-it *weekday-name-vector*)) (string-equal ">=" string :start2 end2 :end2 (incf end2 2)) (digit-char-p (schar string end2)) ;; check for DAY>=NN: (if (and (<= (incf end2) end) (digit-char-p (schar string end2))) (setf end-ptr (1+ end2)) 't))) ;; LastDAY (where DAY is a weekday name): (and (<= (& (1+ end-ptr)) end) (string-equal "last" string :start2 ptr :end2 (+ ptr 4)) (incf& end-ptr) (let* ((start2 (+ ptr 4)) (end2 (+ start2 3))) (flet ((test-it (name) (string-equal name string :start2 start2 :end2 end2))) (declare (dynamic-extent #'test-it)) (find-if #'test-it *weekday-name-vector*)))))) (setf descriptive-date (subseq string ptr end-ptr)) (setf ptr end-ptr) (skip-separators)))) (process-field () (setf result nil) (cond ;; field is numeric: ((process-field-if-numeric) (if (typep result 'fixnum) (if (>& result 31) ; must be a year (if year (not-a-date) (setf year result result nil))) (not-a-date))) ;; field is not numeric: ((and (not month) (process-month-if-alpha))) ((and (not descriptive-date) (process-date-if-alpha))))) (not-a-date () (error "Not a date: ~s" (subseq string start end)))) ;; --------------- ;; DO THE PARSING! (skip-separators) ;; We might-have a day of week, which we skip: (process-possible-day) ;; Process the 1st (required) field: (process-field) (cond (month (setf 1st-field-is-month? 't)) (result (setf 1st-numeric result))) ;; Process the 2nd field: (process-field) (when result (if 1st-numeric (setf 2nd-numeric result) (setf 1st-numeric result))) ;; Process the 3rd field: (process-field) (when result (if 2nd-numeric (setf 3rd-numeric result) (if 1st-numeric (setf 2nd-numeric result) (setf 1st-numeric result)))) ;; Check that all the numerics are fixnums: (when 1st-numeric (unless (typep 1st-numeric 'fixnum) (not-a-date)) (when 2nd-numeric (unless (typep 2nd-numeric 'fixnum) (not-a-date)) (when 3rd-numeric (unless (typep 3rd-numeric 'fixnum) (not-a-date))))) ;; Next, determine what fields (and order) we have! (cond ;; We know the year: (year (cond ;; We know the year and the month: (month (setf date 1st-numeric)) ;; If we have any unresolved numerics, decide what they mean based ;; on value constraints and `month-precedes-date' value: ((or (and 1st-numeric month-precedes-date (<=& 1st-numeric 12)) (and 2nd-numeric (>& 2nd-numeric 12))) (setf month 1st-numeric date 2nd-numeric)) ;; Otherwise, date precedes month: (t (setf date 1st-numeric month 2nd-numeric)))) ;; We know only the month: (month (cond ;; We have the month & 2 numerics: (2nd-numeric (if year-first ;; If `year-first': (setf year 1st-numeric date 2nd-numeric) ;; Otherwise, the date is first: (setf date 1st-numeric year 2nd-numeric))) ;; We have the month & only 1 numeric: (t (cond ;; `year-first' forces the numeric to be the year, unless the ;; month was the 1st field `month-precedes-date': ((and year-first month-precedes-date (not 1st-field-is-month?)) (setf year 1st-numeric)) ;; Assume numeric is the date (in the assumed year): (t (setf date 1st-numeric) (use-assumed-year)))))) ;; We don't know anything yet: (t (cond ;; We have all three fields: (3rd-numeric (if year-first (if (or (and month-precedes-date (<=& 2nd-numeric 12)) (>& 3rd-numeric 12)) (setf year 1st-numeric month 2nd-numeric date 3rd-numeric) (setf year 1st-numeric date 2nd-numeric month 3rd-numeric)) (if (or (and month-precedes-date (<=& 1st-numeric 12)) (>& 2nd-numeric 12)) (setf month 1st-numeric date 2nd-numeric year 3rd-numeric) (setf date 1st-numeric month 2nd-numeric year 3rd-numeric)))) ;; We have only two fields: (2nd-numeric (cond ;; The first numeric is a year: ((and (not 1st-field-is-month?) year-first (<=& 2nd-numeric 12)) (setf year 1st-numeric month 2nd-numeric)) ;; Use assumed year: (t (if (or (and month-precedes-date (<=& 1st-numeric 12)) (>& 2nd-numeric 12)) (setf month 1st-numeric date 2nd-numeric) (setf date 1st-numeric month 2nd-numeric)) (use-assumed-year)))) ;; We don't know the month or the year, and only have 1 numeric: (1st-numeric (cond ;; Assume it is the month, if feasible: ((<=& 1st-numeric 12) (setf month 1st-numeric)) ;; Otherwise, assume it is the date, in the current month: (t (get-decoded-time-unless-cached) (setf date 1st-numeric month current-month))) (use-assumed-year)) ;; We don't have any fields, use the current date: ((not descriptive-date) (get-decoded-time-unless-cached) (setf date current-date month current-month year current-year))))) ;; A month wasn't provided, use January: (unless month (setf month 1)) (if year (maybe-upgrade-year) ;; A year wasn't provided, use the default year: (setf year (use-assumed-year)))) (unless date (cond ;; We were given a descriptive date: (descriptive-date (setf date (convert-descriptive-date descriptive-date month year))) ;; A date wasn't provided, use the 1st of the month: (t (setf date 1)))) (check-type month (integer 1 12)) (check-type date (integer 1 31)) (unless (or junk-allowed (= ptr end)) (junk-in-string-error (subseq string start end))) (values date month year ptr)))) ;;; =========================================================================== ;;; Directories and modules hash tables (defvar *mm-directories* (make-hash-table :test 'eq)) (defvar *mm-modules* (make-hash-table)) ;;; =========================================================================== ;;; Module Directories (defstruct (mm-directory (:conc-name #.(dotted-conc-name 'mm-directory)) (:copier nil)) name documentation (system-name *current-system-name*)) (defstruct (mm-root-directory (:include mm-directory) (:conc-name #.(dotted-conc-name 'mm-root-directory)) (:copier nil)) path application-version-modifier) (defstruct (mm-relative-directory (:include mm-directory) (:conc-name #.(dotted-conc-name 'mm-relative-directory)) (:copier nil)) root subdirectories) ;;; --------------------------------------------------------------------------- (defun get-mm-directory (name) ;; Internal function to get the mm-directory structure given a root or ;; relative directory name (a noop, if an mm-directory is supplied as the ;; name): (if (typep name 'mm-directory) name (or (gethash name *mm-directories*) (error "Directory ~s is not defined." name)))) #-(and lispworks (not lispworks6)) (defmethod documentation (object (doc-type (eql 'directory))) (mm-module.documentation (get-mm-directory object))) #-(and lispworks (not lispworks6)) (defmethod (setf documentation) (nv object (doc-type (eql 'directory))) (setf (mm-module.documentation (get-mm-directory object)) nv)) ;;; =========================================================================== ;;; Directory operators ;;; ;;; Unlike probe-file, probe-directory returns false on a non-directory file. ;;; It should also return true for a symbolic link to a directory. (defun probe-directory (path) #+abcl (ext:file-directory-p path) #+allegro (excl:file-directory-p path) #+clisp (ignore-errors ;; CLISP's probe-directory function signals an error if path is not a ;; directory: (ext:probe-directory path)) #+clozure (let ((pathname (probe-file path))) (and pathname (null (pathname-name pathname)) (null (pathname-type pathname)))) #+cmu (let ((pathname (probe-file path))) (and pathname (null (pathname-name pathname)) (null (pathname-type pathname)))) #+cormanlisp (cormanlisp:directory-p path) #+digitool-mcl (let ((pathname (probe-file path))) (and pathname (null (pathname-name pathname)) (null (pathname-type pathname)))) #+ecl (let ((pathname (probe-file path))) (and pathname (null (pathname-name pathname)) (null (pathname-type pathname)))) #+gcl ;; GCL's probe-file returns nil on directories, but directory returns ;; the directory (on linux, at least): (and (not (probe-file path)) (directory path)) #+lispworks (system::file-directory-p path) #+sbcl (let ((pathname (probe-file path))) (and pathname (null (pathname-name pathname)) (null (pathname-type pathname)))) #+scl (let ((pathname (probe-file path))) (and pathname (null (pathname-name pathname)) (null (pathname-type pathname)))) #-(or abcl allegro clisp clozure cmu cormanlisp digitool-mcl ecl gcl lispworks sbcl scl xcl) (need-to-port probe-directory)) ;;; --------------------------------------------------------------------------- (defun non-keyword-directory-name-error (name) (error "Directory name, ~s, must be a keyword." name)) ;;; --------------------------------------------------------------------------- (defun non-keyword-root/relative-directory-name-error (name) (error "Root or relative directory name, ~s, must be a keyword." name)) ;;; --------------------------------------------------------------------------- (defun get-mm-root-directory (name) (let ((mm-dir (gethash name *mm-directories*))) (typecase mm-dir (mm-root-directory mm-dir) (t (error "Root directory ~s is not defined." name))))) ;;; --------------------------------------------------------------------------- (defun append-subdirectories (directory &rest subdirectory-lists) ;; Process pathname-directory :up keywords ourselves, to keep things pretty ;; on CLs that don't normalize aggressively (we'll leave :back ;; keywords--should anyone use them--to CL to deal with): (declare (dynamic-extent subdirectory-lists)) (let ((directory (reverse directory))) (dolist (subdirectories subdirectory-lists) (dolist (subdirectory subdirectories) (cond ((and (eq subdirectory ':up) (stringp (first directory))) (pop directory)) (t (push subdirectory directory))))) (nreverse directory))) ;;; --------------------------------------------------------------------------- (defun compute-root-directory (spec subdirectories) (flet ((compute-it (spec) (let ((root-pathname (etypecase spec (pathname (make-pathname :name nil :type nil :defaults spec)) (string (pathname spec)) (mm-root-directory (mm-root-directory.path spec))))) (make-pathname :directory (append-subdirectories (pathname-directory root-pathname) subdirectories) :defaults root-pathname)))) (typecase spec (symbol (compute-it (if (keywordp spec) (get-mm-root-directory spec) (symbol-value spec)))) (otherwise (compute-it spec))))) ;;; --------------------------------------------------------------------------- (defun define-root-directory (name &rest args) (let* ((documentation (when (stringp (first args)) (pop args))) (spec (pop args)) (subdirectories args) (application-version-modifier nil)) (when (consp name) (setf application-version-modifier (second name)) (setf name (first name))) (unless (keywordp name) (non-keyword-directory-name-error name)) (let ((root-directory-path (compute-root-directory spec subdirectories))) (setf (gethash name *mm-directories*) (make-mm-root-directory :name name :documentation documentation :path root-directory-path :application-version-modifier application-version-modifier))))) ;;; --------------------------------------------------------------------------- (defun define-relative-directory (name &rest args) (let* ((documentation (when (stringp (first args)) (pop args))) (root (pop args)) (subdirectories args)) (unless (keywordp name) (non-keyword-directory-name-error name)) (unless (keywordp root) (non-keyword-root/relative-directory-name-error root)) (setf (gethash name *mm-directories*) (make-mm-relative-directory :name name :documentation documentation :root root :subdirectories subdirectories)))) ;;; --------------------------------------------------------------------------- (defun make-and-check-directory-pathname (name subdirectories compiled? application-version-modifier patches?) ;;; Used by compute-relative-directory to handle various "source" ;;; directory-name conventions: (labels ((make-directory-pathname (subtree-name &optional skip-subdirectories?) (make-pathname :directory (append-subdirectories (pathname-directory name) (list subtree-name) (unless skip-subdirectories? (if patches? (append subdirectories '("patches")) subdirectories))) :defaults name))) (cond ;; If compiled?, make and return the pathname (concatenating ;; application-version modifier, if appropriate): (compiled? (make-directory-pathname (if application-version-modifier ;; Concatenate the version modifier to the compiled-directory name: (concatenate 'simple-string *compiled-directory-name* "-" application-version-modifier) *compiled-directory-name*))) ;; Source directory: check what "source" directory name is needed before ;; making and returning the pathname: (t (cond ;; Regular "source": ((probe-directory (make-directory-pathname "source" t)) (make-directory-pathname "source")) ;; "Source": ((probe-directory (make-directory-pathname "Source" t)) (make-directory-pathname "Source")) ;; "SOURCE": ((probe-directory (make-directory-pathname "SOURCE" t)) (make-directory-pathname "SOURCE")) ;; Otherwise, we'll just use "source": (t (make-directory-pathname "source"))))))) ;;; --------------------------------------------------------------------------- ;; CMUCL 19e and SCL complain about the following declaration: #-(or cmu scl) (declaim (ftype (function (mm-module) (values t &optional)) mm-module.directory mm-module.subdirectories)) (defun compute-relative-directory (name subdirectories compiled? &optional patches?) (let ((in-process nil)) (labels ((compute-it (name subdirectories) (cond ((null name) nil) ;; `Name' can be a pathname if a *load-truename*-relative ;; :directory option was used in define-module: ((pathnamep name) (make-and-check-directory-pathname name subdirectories compiled? nil patches?)) (t (let ((mm-dir (gethash name *mm-directories*))) (typecase mm-dir (mm-relative-directory (when (member name in-process :test #'eq) (error "Circularity in relative-directory relation:~ ~%~3t~{~s -> ~}~s" (reverse in-process) name)) (push name in-process) (compute-it (mm-relative-directory.root mm-dir) (append-subdirectories (mm-relative-directory.subdirectories mm-dir) subdirectories))) (mm-root-directory (let ((root-path (mm-root-directory.path mm-dir)) (application-version-modifier (mm-root-directory.application-version-modifier mm-dir))) (make-and-check-directory-pathname root-path subdirectories compiled? application-version-modifier patches?))) (otherwise (let ((module ;; Check if we have a module reference (look ;; without the get-module error check): (gethash name *mm-modules*))) (cond ;; The reference is module relative: (module (when (eq name (mm-module.directory module)) (error "Directory ~s is defined in terms of itself" name)) (compute-relative-directory (mm-module.directory module) (append-subdirectories (mm-module.subdirectories module) subdirectories) compiled? patches?)) (t (error "Directory ~s is not defined." name))))))))))) (compute-it name subdirectories)))) ;;; --------------------------------------------------------------------------- (defun undefine-directory (name) (if (gethash name *mm-directories*) (remhash name *mm-directories*) (warn "Directory ~s is not defined." name))) ;;; --------------------------------------------------------------------------- (defun show-defined-directories () (cond ((zerop (& (hash-table-count *mm-directories*))) (format t "~& No directories are defined.~%")) (t (let ((directories nil)) (maphash #'(lambda (key directory) (declare (ignore key)) (push directory directories)) *mm-directories*) (dolist (directory (sort directories #'string-lessp :key #'mm-directory.name)) (typecase directory (mm-root-directory (format t "~&~s~%~4tRoot: ~a" (mm-directory.name directory) (mm-root-directory.path directory))) (t (let ((root-name (mm-relative-directory.root directory))) (format t "~&~s~%~4tRelative to~:[ module~;~] ~(~s~)~ ~%~4tsubdirectories: ~s" (mm-directory.name directory) (gethash root-name *mm-directories*) root-name (mm-relative-directory.subdirectories directory)))))) (terpri)))) (terpri) (values)) ;;; =========================================================================== ;;; Modules (defvar *skip-requires-ordering-check* nil) (defvar *deferred-requires-ordering-check-module-names* nil) ;;; --------------------------------------------------------------------------- (defstruct (mm-module (:conc-name #.(dotted-conc-name 'mm-module)) (:copier nil)) ;;; NOTE: Changes to slots must be reflected in ENSURE-MODULE: name (documentation nil) (directory nil) (subdirectories) (requires nil) (files nil) (files-loaded nil) (patches nil) (load-completed? nil) (latest-forces-recompiled-date 0) ;; undocumented (used for compile-gbbopen exit): (after-form nil) (system-name *current-system-name*) (patch-descriptions nil) (frozen? nil)) ;;; --------------------------------------------------------------------------- (defmethod print-object ((object mm-module) stream) (cond (*print-readably* (call-next-method)) (t (print-unreadable-object (object stream :type t) (format stream "~:[~;% ~]~s" (mm-module.frozen? object) (mm-module.name object))) ;; Print-object must return object: object))) ;;; --------------------------------------------------------------------------- ;;; Module object's aren't documented, so we use DOCUMENTATION methods to ;;; set and retrieve module documentation strings: (defmethod documentation (object (doc-type (eql 'module))) (mm-module.documentation (get-module object))) (defmethod (setf documentation) (nv object (doc-type (eql 'module))) (setf (mm-module.documentation (get-module object)) nv)) ;;; --------------------------------------------------------------------------- (defmacro with-module-redefinitions (&body body) ;; skip requires-ordering checks as we go: `(let ((*skip-requires-ordering-check* 't) (*deferred-requires-ordering-check-module-names* nil)) ,@body ;; but do check them at the end: (check-all-module-requires-orderings :module-names ;; Check them in "as seen" order: (nreverse *deferred-requires-ordering-check-module-names*)))) ;;; --------------------------------------------------------------------------- (defmacro define-module (name &body args) (unless (keywordp name) (error "Module name, ~s, must be a keyword." name)) (let ((documentation (when (stringp (car args)) (pop args))) (directory nil) (directory-seen? nil) (subdirectories nil) (requires nil) (requires-seen? nil) (files nil) (files-seen? nil) (patches nil) (patches-seen? nil) (after-form nil) (after-form-seen? nil)) (dolist (option args) (unless (and (consp option) (keywordp (first option))) (error "Badly formed option, ~s, in module ~s.~_~ Each option must be a list of the form (