tconfiguration.lisp - clic - Clic is an command line interactive client for gopher written in Common LISP
 (HTM) git clone git://bitreich.org/clic/ git://hg6vgqziawt5s4dj.onion/clic/
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Tags
 (DIR) LICENSE
       ---
       tconfiguration.lisp (21324B)
       ---
            1 ;;;; ---------------------------------------------------------------------------
            2 ;;;; Generic support for configuration files
            3 
            4 (uiop/package:define-package :uiop/configuration
            5   (:recycle :uiop/configuration :asdf/configuration) ;; necessary to upgrade from 2.27.
            6   (:use :uiop/common-lisp :uiop/utility
            7    :uiop/os :uiop/pathname :uiop/filesystem :uiop/stream :uiop/image :uiop/lisp-build)
            8   (:export
            9    #:user-configuration-directories #:system-configuration-directories ;; implemented in backward-driver
           10    #:in-first-directory #:in-user-configuration-directory #:in-system-configuration-directory ;; idem
           11    #:get-folder-path
           12    #:xdg-data-home #:xdg-config-home #:xdg-data-dirs #:xdg-config-dirs
           13    #:xdg-cache-home #:xdg-runtime-dir #:system-config-pathnames
           14    #:filter-pathname-set #:xdg-data-pathnames #:xdg-config-pathnames
           15    #:find-preferred-file #:xdg-data-pathname #:xdg-config-pathname
           16    #:validate-configuration-form #:validate-configuration-file #:validate-configuration-directory
           17    #:configuration-inheritance-directive-p
           18    #:report-invalid-form #:invalid-configuration #:*ignored-configuration-form* #:*user-cache*
           19    #:*clear-configuration-hook* #:clear-configuration #:register-clear-configuration-hook
           20    #:resolve-location #:location-designator-p #:location-function-p #:*here-directory*
           21    #:resolve-relative-location #:resolve-absolute-location #:upgrade-configuration))
           22 (in-package :uiop/configuration)
           23 
           24 (with-upgradability ()
           25   (define-condition invalid-configuration ()
           26     ((form :reader condition-form :initarg :form)
           27      (location :reader condition-location :initarg :location)
           28      (format :reader condition-format :initarg :format)
           29      (arguments :reader condition-arguments :initarg :arguments :initform nil))
           30     (:report (lambda (c s)
           31                (format s (compatfmt "~@<~? (will be skipped)~@:>")
           32                        (condition-format c)
           33                        (list* (condition-form c) (condition-location c)
           34                               (condition-arguments c))))))
           35 
           36   (defun configuration-inheritance-directive-p (x)
           37     "Is X a configuration inheritance directive?"
           38     (let ((kw '(:inherit-configuration :ignore-inherited-configuration)))
           39       (or (member x kw)
           40           (and (length=n-p x 1) (member (car x) kw)))))
           41 
           42   (defun report-invalid-form (reporter &rest args)
           43     "Report an invalid form according to REPORTER and various ARGS"
           44     (etypecase reporter
           45       (null
           46        (apply 'error 'invalid-configuration args))
           47       (function
           48        (apply reporter args))
           49       ((or symbol string)
           50        (apply 'error reporter args))
           51       (cons
           52        (apply 'apply (append reporter args)))))
           53 
           54   (defvar *ignored-configuration-form* nil
           55     "Have configuration forms been ignored while parsing the configuration?")
           56 
           57   (defun validate-configuration-form (form tag directive-validator
           58                                             &key location invalid-form-reporter)
           59     "Validate a configuration FORM. By default it will raise an error if the
           60 FORM is not valid.  Otherwise it will return the validated form.
           61      Arguments control the behavior:
           62      The configuration FORM should be of the form (TAG . <rest>)
           63      Each element of <rest> will be checked by first seeing if it's a configuration inheritance
           64 directive (see CONFIGURATION-INHERITANCE-DIRECTIVE-P) then invoking DIRECTIVE-VALIDATOR
           65 on it.
           66      In the event of an invalid form, INVALID-FORM-REPORTER will be used to control
           67 reporting (see REPORT-INVALID-FORM) with LOCATION providing information about where
           68 the configuration form appeared."
           69     (unless (and (consp form) (eq (car form) tag))
           70       (setf *ignored-configuration-form* t)
           71       (report-invalid-form invalid-form-reporter :form form :location location)
           72       (return-from validate-configuration-form nil))
           73     (loop :with inherit = 0 :with ignore-invalid-p = nil :with x = (list tag)
           74           :for directive :in (cdr form)
           75           :when (cond
           76                   ((configuration-inheritance-directive-p directive)
           77                    (incf inherit) t)
           78                   ((eq directive :ignore-invalid-entries)
           79                    (setf ignore-invalid-p t) t)
           80                   ((funcall directive-validator directive)
           81                    t)
           82                   (ignore-invalid-p
           83                    nil)
           84                   (t
           85                    (setf *ignored-configuration-form* t)
           86                    (report-invalid-form invalid-form-reporter :form directive :location location)
           87                    nil))
           88             :do (push directive x)
           89           :finally
           90              (unless (= inherit 1)
           91                (report-invalid-form invalid-form-reporter
           92                                     :form form :location location
           93                                     ;; we throw away the form and location arguments, hence the ~2*
           94                                     ;; this is necessary because of the report in INVALID-CONFIGURATION
           95                                     :format (compatfmt "~@<Invalid source registry ~S~@[ in ~S~]. ~
           96                                                         One and only one of ~S or ~S is required.~@:>")
           97                                     :arguments '(:inherit-configuration :ignore-inherited-configuration)))
           98              (return (nreverse x))))
           99 
          100   (defun validate-configuration-file (file validator &key description)
          101     "Validate a configuration FILE.  The configuration file should have only one s-expression
          102 in it, which will be checked with the VALIDATOR FORM.  DESCRIPTION argument used for error
          103 reporting."
          104     (let ((forms (read-file-forms file)))
          105       (unless (length=n-p forms 1)
          106         (error (compatfmt "~@<One and only one form allowed for ~A. Got: ~3i~_~S~@:>~%")
          107                description forms))
          108       (funcall validator (car forms) :location file)))
          109 
          110   (defun validate-configuration-directory (directory tag validator &key invalid-form-reporter)
          111     "Map the VALIDATOR across the .conf files in DIRECTORY, the TAG will
          112 be applied to the results to yield a configuration form.  Current
          113 values of TAG include :source-registry and :output-translations."
          114     (let ((files (sort (ignore-errors ;; SORT w/o COPY-LIST is OK: DIRECTORY returns a fresh list
          115                         (remove-if
          116                          'hidden-pathname-p
          117                          (directory* (make-pathname :name *wild* :type "conf" :defaults directory))))
          118                        #'string< :key #'namestring)))
          119       `(,tag
          120         ,@(loop :for file :in files :append
          121                                     (loop :with ignore-invalid-p = nil
          122                                           :for form :in (read-file-forms file)
          123                                           :when (eq form :ignore-invalid-entries)
          124                                             :do (setf ignore-invalid-p t)
          125                                           :else
          126                                             :when (funcall validator form)
          127                                               :collect form
          128                                           :else
          129                                             :when ignore-invalid-p
          130                                               :do (setf *ignored-configuration-form* t)
          131                                           :else
          132                                             :do (report-invalid-form invalid-form-reporter :form form :location file)))
          133         :inherit-configuration)))
          134 
          135   (defun resolve-relative-location (x &key ensure-directory wilden)
          136     "Given a designator X for an relative location, resolve it to a pathname."
          137     (ensure-pathname
          138      (etypecase x
          139        (null nil)
          140        (pathname x)
          141        (string (parse-unix-namestring
          142                 x :ensure-directory ensure-directory))
          143        (cons
          144         (if (null (cdr x))
          145             (resolve-relative-location
          146              (car x) :ensure-directory ensure-directory :wilden wilden)
          147             (let* ((car (resolve-relative-location
          148                          (car x) :ensure-directory t :wilden nil)))
          149               (merge-pathnames*
          150                (resolve-relative-location
          151                 (cdr x) :ensure-directory ensure-directory :wilden wilden)
          152                car))))
          153        ((eql :*/) *wild-directory*)
          154        ((eql :**/) *wild-inferiors*)
          155        ((eql :*.*.*) *wild-file*)
          156        ((eql :implementation)
          157         (parse-unix-namestring
          158          (implementation-identifier) :ensure-directory t))
          159        ((eql :implementation-type)
          160         (parse-unix-namestring
          161          (string-downcase (implementation-type)) :ensure-directory t))
          162        ((eql :hostname)
          163         (parse-unix-namestring (hostname) :ensure-directory t)))
          164      :wilden (and wilden (not (pathnamep x)) (not (member x '(:*/ :**/ :*.*.*))))
          165      :want-relative t))
          166 
          167   (defvar *here-directory* nil
          168     "This special variable is bound to the currect directory during calls to
          169 PROCESS-SOURCE-REGISTRY in order that we be able to interpret the :here
          170 directive.")
          171 
          172   (defvar *user-cache* nil
          173     "A specification as per RESOLVE-LOCATION of where the user keeps his FASL cache")
          174 
          175   (defun resolve-absolute-location (x &key ensure-directory wilden)
          176     "Given a designator X for an absolute location, resolve it to a pathname"
          177     (ensure-pathname
          178      (etypecase x
          179        (null nil)
          180        (pathname x)
          181        (string
          182         (let ((p #-mcl (parse-namestring x)
          183                  #+mcl (probe-posix x)))
          184           #+mcl (unless p (error "POSIX pathname ~S does not exist" x))
          185           (if ensure-directory (ensure-directory-pathname p) p)))
          186        (cons
          187         (return-from resolve-absolute-location
          188           (if (null (cdr x))
          189               (resolve-absolute-location
          190                (car x) :ensure-directory ensure-directory :wilden wilden)
          191               (merge-pathnames*
          192                (resolve-relative-location
          193                 (cdr x) :ensure-directory ensure-directory :wilden wilden)
          194                (resolve-absolute-location
          195                 (car x) :ensure-directory t :wilden nil)))))
          196        ((eql :root)
          197         ;; special magic! we return a relative pathname,
          198         ;; but what it means to the output-translations is
          199         ;; "relative to the root of the source pathname's host and device".
          200         (return-from resolve-absolute-location
          201           (let ((p (make-pathname :directory '(:relative))))
          202             (if wilden (wilden p) p))))
          203        ((eql :home) (user-homedir-pathname))
          204        ((eql :here) (resolve-absolute-location
          205                      (or *here-directory* (pathname-directory-pathname (load-pathname)))
          206                      :ensure-directory t :wilden nil))
          207        ((eql :user-cache) (resolve-absolute-location
          208                            *user-cache* :ensure-directory t :wilden nil)))
          209      :wilden (and wilden (not (pathnamep x)))
          210      :resolve-symlinks *resolve-symlinks*
          211      :want-absolute t))
          212 
          213   ;; Try to override declaration in previous versions of ASDF.
          214   (declaim (ftype (function (t &key (:directory boolean) (:wilden boolean)
          215                                (:ensure-directory boolean)) t) resolve-location))
          216 
          217   (defun* (resolve-location) (x &key ensure-directory wilden directory)
          218     "Resolve location designator X into a PATHNAME"
          219     ;; :directory backward compatibility, until 2014-01-16: accept directory as well as ensure-directory
          220     (loop* :with dirp = (or directory ensure-directory)
          221            :with (first . rest) = (if (atom x) (list x) x)
          222            :with path = (or (resolve-absolute-location
          223                              first :ensure-directory (and (or dirp rest) t)
          224                                    :wilden (and wilden (null rest)))
          225                             (return nil))
          226            :for (element . morep) :on rest
          227            :for dir = (and (or morep dirp) t)
          228            :for wild = (and wilden (not morep))
          229            :for sub = (merge-pathnames*
          230                        (resolve-relative-location
          231                         element :ensure-directory dir :wilden wild)
          232                        path)
          233            :do (setf path (if (absolute-pathname-p sub) (resolve-symlinks* sub) sub))
          234            :finally (return path)))
          235 
          236   (defun location-designator-p (x)
          237     "Is X a designator for a location?"
          238     ;; NIL means "skip this entry", or as an output translation, same as translation input.
          239     ;; T means "any input" for a translation, or as output, same as translation input.
          240     (flet ((absolute-component-p (c)
          241              (typep c '(or string pathname
          242                         (member :root :home :here :user-cache))))
          243            (relative-component-p (c)
          244              (typep c '(or string pathname
          245                         (member :*/ :**/ :*.*.* :implementation :implementation-type)))))
          246       (or (typep x 'boolean)
          247           (absolute-component-p x)
          248           (and (consp x) (absolute-component-p (first x)) (every #'relative-component-p (rest x))))))
          249 
          250   (defun location-function-p (x)
          251     "Is X the specification of a location function?"
          252     ;; Location functions are allowed in output translations, and notably used by ABCL for JAR file support.
          253     (and (length=n-p x 2) (eq (car x) :function)))
          254 
          255   (defvar *clear-configuration-hook* '())
          256 
          257   (defun register-clear-configuration-hook (hook-function &optional call-now-p)
          258     "Register a function to be called when clearing configuration"
          259     (register-hook-function '*clear-configuration-hook* hook-function call-now-p))
          260 
          261   (defun clear-configuration ()
          262     "Call the functions in *CLEAR-CONFIGURATION-HOOK*"
          263     (call-functions *clear-configuration-hook*))
          264 
          265   (register-image-dump-hook 'clear-configuration)
          266 
          267   (defun upgrade-configuration ()
          268     "If a previous version of ASDF failed to read some configuration, try again now."
          269     (when *ignored-configuration-form*
          270       (clear-configuration)
          271       (setf *ignored-configuration-form* nil)))
          272 
          273 
          274   (defun get-folder-path (folder)
          275     "Semi-portable implementation of a subset of LispWorks' sys:get-folder-path,
          276 this function tries to locate the Windows FOLDER for one of
          277 :LOCAL-APPDATA, :APPDATA or :COMMON-APPDATA.
          278      Returns NIL when the folder is not defined (e.g., not on Windows)."
          279     (or #+(and lispworks os-windows) (sys:get-folder-path folder)
          280         ;; read-windows-registry HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders\AppData
          281         (ecase folder
          282           (:local-appdata (or (getenv-absolute-directory "LOCALAPPDATA")
          283                               (subpathname* (get-folder-path :appdata) "Local")))
          284           (:appdata (getenv-absolute-directory "APPDATA"))
          285           (:common-appdata (or (getenv-absolute-directory "ALLUSERSAPPDATA")
          286                                (subpathname* (getenv-absolute-directory "ALLUSERSPROFILE") "Application Data/"))))))
          287 
          288 
          289   ;; Support for the XDG Base Directory Specification
          290   (defun xdg-data-home (&rest more)
          291     "Returns an absolute pathname for the directory containing user-specific data files.
          292 MORE may contain specifications for a subpath relative to this directory: a
          293 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          294 also \"Configuration DSL\"\) in the ASDF manual."
          295     (resolve-absolute-location
          296      `(,(or (getenv-absolute-directory "XDG_DATA_HOME")
          297             (os-cond
          298              ((os-windows-p) (get-folder-path :local-appdata))
          299              (t (subpathname (user-homedir-pathname) ".local/share/"))))
          300        ,more)))
          301 
          302   (defun xdg-config-home (&rest more)
          303     "Returns a pathname for the directory containing user-specific configuration files.
          304 MORE may contain specifications for a subpath relative to this directory: a
          305 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          306 also \"Configuration DSL\"\) in the ASDF manual."
          307     (resolve-absolute-location
          308      `(,(or (getenv-absolute-directory "XDG_CONFIG_HOME")
          309             (os-cond
          310              ((os-windows-p) (xdg-data-home "config/"))
          311              (t (subpathname (user-homedir-pathname) ".config/"))))
          312        ,more)))
          313 
          314   (defun xdg-data-dirs (&rest more)
          315     "The preference-ordered set of additional paths to search for data files.
          316 Returns a list of absolute directory pathnames.
          317 MORE may contain specifications for a subpath relative to these directories: a
          318 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          319 also \"Configuration DSL\"\) in the ASDF manual."
          320     (mapcar #'(lambda (d) (resolve-location `(,d ,more)))
          321             (or (remove nil (getenv-absolute-directories "XDG_DATA_DIRS"))
          322                 (os-cond
          323                  ((os-windows-p) (mapcar 'get-folder-path '(:appdata :common-appdata)))
          324                  (t (mapcar 'parse-unix-namestring '("/usr/local/share/" "/usr/share/")))))))
          325 
          326   (defun xdg-config-dirs (&rest more)
          327     "The preference-ordered set of additional base paths to search for configuration files.
          328 Returns a list of absolute directory pathnames.
          329 MORE may contain specifications for a subpath relative to these directories:
          330 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          331 also \"Configuration DSL\"\) in the ASDF manual."
          332     (mapcar #'(lambda (d) (resolve-location `(,d ,more)))
          333             (or (remove nil (getenv-absolute-directories "XDG_CONFIG_DIRS"))
          334                 (os-cond
          335                  ((os-windows-p) (xdg-data-dirs "config/"))
          336                  (t (mapcar 'parse-unix-namestring '("/etc/xdg/")))))))
          337 
          338   (defun xdg-cache-home (&rest more)
          339     "The base directory relative to which user specific non-essential data files should be stored.
          340 Returns an absolute directory pathname.
          341 MORE may contain specifications for a subpath relative to this directory: a
          342 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          343 also \"Configuration DSL\"\) in the ASDF manual."
          344     (resolve-absolute-location
          345      `(,(or (getenv-absolute-directory "XDG_CACHE_HOME")
          346             (os-cond
          347              ((os-windows-p) (xdg-data-home "cache/"))
          348              (t (subpathname* (user-homedir-pathname) ".cache/"))))
          349        ,more)))
          350 
          351   (defun xdg-runtime-dir (&rest more)
          352     "Pathname for user-specific non-essential runtime files and other file objects,
          353 such as sockets, named pipes, etc.
          354 Returns an absolute directory pathname.
          355 MORE may contain specifications for a subpath relative to this directory: a
          356 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          357 also \"Configuration DSL\"\) in the ASDF manual."
          358     ;; The XDG spec says that if not provided by the login system, the application should
          359     ;; issue a warning and provide a replacement. UIOP is not equipped to do that and returns NIL.
          360     (resolve-absolute-location `(,(getenv-absolute-directory "XDG_RUNTIME_DIR") ,more)))
          361 
          362   ;;; NOTE: modified the docstring because "system user configuration
          363   ;;; directories" seems self-contradictory. I'm not sure my wording is right.
          364   (defun system-config-pathnames (&rest more)
          365     "Return a list of directories where are stored the system's default user configuration information.
          366 MORE may contain specifications for a subpath relative to these directories: a
          367 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          368 also \"Configuration DSL\"\) in the ASDF manual."
          369     (declare (ignorable more))
          370     (os-cond
          371      ((os-unix-p) (list (resolve-absolute-location `(,(parse-unix-namestring "/etc/") ,more))))))
          372 
          373   (defun filter-pathname-set (dirs)
          374     "Parse strings as unix namestrings and remove duplicates and non absolute-pathnames in a list."
          375     (remove-duplicates (remove-if-not #'absolute-pathname-p dirs) :from-end t :test 'equal))
          376 
          377   (defun xdg-data-pathnames (&rest more)
          378     "Return a list of absolute pathnames for application data directories.  With APP,
          379 returns directory for data for that application, without APP, returns the set of directories
          380 for storing all application configurations.
          381 MORE may contain specifications for a subpath relative to these directories: a
          382 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          383 also \"Configuration DSL\"\) in the ASDF manual."
          384     (filter-pathname-set
          385      `(,(xdg-data-home more)
          386        ,@(xdg-data-dirs more))))
          387 
          388   (defun xdg-config-pathnames (&rest more)
          389     "Return a list of pathnames for application configuration.
          390 MORE may contain specifications for a subpath relative to these directories: a
          391 subpathname specification and keyword arguments as per RESOLVE-LOCATION \(see
          392 also \"Configuration DSL\"\) in the ASDF manual."
          393     (filter-pathname-set
          394      `(,(xdg-config-home more)
          395        ,@(xdg-config-dirs more))))
          396 
          397   (defun find-preferred-file (files &key (direction :input))
          398     "Find first file in the list of FILES that exists (for direction :input or :probe)
          399 or just the first one (for direction :output or :io).
          400     Note that when we say \"file\" here, the files in question may be directories."
          401     (find-if (ecase direction ((:probe :input) 'probe-file*) ((:output :io) 'identity)) files))
          402 
          403   (defun xdg-data-pathname (&optional more (direction :input))
          404     (find-preferred-file (xdg-data-pathnames more) :direction direction))
          405 
          406   (defun xdg-config-pathname (&optional more (direction :input))
          407     (find-preferred-file (xdg-config-pathnames more) :direction direction))
          408 
          409   (defun compute-user-cache ()
          410     "Compute (and return) the location of the default user-cache for translate-output
          411 objects. Side-effects for cached file location computation."
          412     (setf *user-cache* (xdg-cache-home "common-lisp" :implementation)))
          413   (register-image-restore-hook 'compute-user-cache))