lisp-utils.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016, 2017 Andy Patterson <ajpatter@uwaterloo.ca>
  3. ;;; Copyright © 2020, 2022 Guillaume Le Vaillant <glv@posteo.net>
  4. ;;; Copyright © 2022 Pierre Neidhardt <mail@ambrevar.xyz>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (guix build lisp-utils)
  21. #:use-module (ice-9 format)
  22. #:use-module (ice-9 hash-table)
  23. #:use-module (ice-9 match)
  24. #:use-module (ice-9 regex)
  25. #:use-module (srfi srfi-1)
  26. #:use-module (srfi srfi-26)
  27. #:use-module (guix build utils)
  28. #:export (%lisp
  29. %lisp-type
  30. %source-install-prefix
  31. lisp-eval-program
  32. compile-systems
  33. test-system
  34. replace-escaped-macros
  35. generate-executable-wrapper-system
  36. generate-executable-entry-point
  37. generate-executable-for-system
  38. wrap-output-translations
  39. prepend-to-source-registry
  40. build-program
  41. build-image
  42. make-asdf-configuration
  43. valid-char-set
  44. normalize-string
  45. library-output))
  46. ;;; Commentary:
  47. ;;;
  48. ;;; Tools to evaluate lisp programs within a lisp session, generate wrapper
  49. ;;; systems for executables. Compile, test, and produce images for systems and
  50. ;;; programs, and link them with their dependencies.
  51. ;;;
  52. ;;; Code:
  53. (define %lisp
  54. ;; File name of the Lisp compiler.
  55. (make-parameter "lisp"))
  56. (define %lisp-type
  57. ;; String representing the class of implementation being used.
  58. (make-parameter "lisp"))
  59. ;; The common parent for Lisp source files, as will as the symbolic
  60. ;; link farm for system definition (.asd) files.
  61. (define %source-install-prefix "/share/common-lisp")
  62. (define (library-output outputs)
  63. "If a `lib' output exists, build things there. Otherwise use `out'."
  64. (or (assoc-ref outputs "lib") (assoc-ref outputs "out")))
  65. ;; See nix/libstore/store-api.cc#checkStoreName.
  66. (define valid-char-set
  67. (string->char-set
  68. "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-._?="))
  69. (define (normalize-string str)
  70. "Replace invalid characters in STR with a hyphen."
  71. (string-join (string-tokenize str valid-char-set) "-"))
  72. (define (wrap-output-translations translations)
  73. `(:output-translations
  74. ,@translations
  75. :inherit-configuration))
  76. (define (lisp-eval-program program)
  77. "Evaluate PROGRAM with a given LISP implementation."
  78. (define invocation (lisp-invocation program))
  79. (format #t "Invoking ~a: ~{~s ~}~%" (%lisp-type) invocation)
  80. (apply invoke invocation))
  81. (define (spread-statements program argument-name)
  82. "Return a list with the statements from PROGRAM spread between
  83. ARGUMENT-NAME, a string representing the argument a lisp implementation uses
  84. to accept statements to be evaluated before starting."
  85. (append-map (lambda (statement)
  86. (list argument-name (format #f "~S" statement)))
  87. program))
  88. (define (lisp-invocation program)
  89. "Return a list of arguments for system* determining how to invoke LISP
  90. with PROGRAM."
  91. (match (%lisp-type)
  92. ("sbcl" `(,(%lisp) "--non-interactive"
  93. ,@(spread-statements program "--eval")))
  94. ("ecl" `(,(%lisp)
  95. ,@(spread-statements program "--eval")
  96. "--eval" "(quit)"))
  97. (_ (error "The LISP provided is not supported at this time."))))
  98. (define (compile-systems systems directory operation)
  99. "Use a lisp implementation to compile the SYSTEMS using asdf."
  100. (lisp-eval-program
  101. `((require :asdf)
  102. (asdf:initialize-source-registry
  103. (list :source-registry (list :tree (uiop:ensure-pathname ,directory
  104. :truenamize t
  105. :ensure-directory t))
  106. :inherit-configuration))
  107. ,@(map (lambda (system)
  108. (list (string->symbol (string-append "asdf:" operation)) system))
  109. systems))))
  110. (define (test-system test-systems directory)
  111. "Use a lisp implementation to test the TEST-SYSTEMS using asdf."
  112. (lisp-eval-program
  113. `((require :asdf)
  114. (asdf:initialize-source-registry
  115. (list :source-registry (list :tree (uiop:ensure-pathname ,directory
  116. :truenamize t
  117. :ensure-directory t))
  118. :inherit-configuration))
  119. ,@(map (lambda (system)
  120. `(asdf:test-system ,system))
  121. test-systems))))
  122. (define (string->lisp-keyword . strings)
  123. "Return a lisp keyword for the concatenation of STRINGS."
  124. (string->symbol (apply string-append ":" strings)))
  125. (define* (generate-executable-for-system type system #:key compress?)
  126. "Use LISP to generate an executable, whose TYPE can be 'asdf:image-op or
  127. 'asdf:program-op. The latter will always be standalone. Depends on having
  128. created a \"SYSTEM-exec\" system which contains the entry program."
  129. (lisp-eval-program
  130. `((require :asdf)
  131. ;; Only SBCL supports compression as of 2019-09-02.
  132. ,(if (and compress? (string=? (%lisp-type) "sbcl"))
  133. '(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
  134. (uiop:dump-image (asdf:output-file o c)
  135. :executable t
  136. :compression t))
  137. '())
  138. (asdf:load-asd (truename ,(string-append system "-exec.asd")))
  139. (asdf:operate ',type ,(string-append system "-exec")))))
  140. (define (generate-executable-wrapper-system system dependencies)
  141. "Generates a system which can be used by asdf to produce an image or program
  142. inside the current directory. The image or program will contain
  143. DEPENDENCIES."
  144. (with-output-to-file (string-append system "-exec.asd")
  145. (lambda _
  146. (format #t "~y~%"
  147. `(defsystem ,(string->lisp-keyword system "-exec")
  148. :entry-point ,(string-append system "-exec:main")
  149. :depends-on (:uiop
  150. ,@(map string->lisp-keyword
  151. dependencies))
  152. :components ((:file ,(string-append system "-exec"))))))))
  153. (define (generate-executable-entry-point system entry-program)
  154. "Generates an entry point program from the list of lisp statements
  155. ENTRY-PROGRAM for SYSTEM within the current directory."
  156. (with-output-to-file (string-append system "-exec.lisp")
  157. (lambda _
  158. (let ((system (string->lisp-keyword system "-exec")))
  159. (format #t "~{~y~%~%~}"
  160. `((defpackage ,system
  161. (:use :cl)
  162. (:export :main))
  163. (in-package ,system)
  164. (defun main ()
  165. (let ((arguments uiop:*command-line-arguments*))
  166. (declare (ignorable arguments))
  167. ,@entry-program))))))))
  168. (define (make-asdf-configuration name conf-dir deps-conf-dir source-dir lib-dir)
  169. (let ((registry-dir (string-append
  170. conf-dir "/source-registry.conf.d"))
  171. (translations-dir (string-append
  172. conf-dir "/asdf-output-translations.conf.d"))
  173. (deps-registry-dir (string-append
  174. deps-conf-dir "/source-registry.conf.d"))
  175. (deps-translations-dir (string-append
  176. deps-conf-dir
  177. "/asdf-output-translations.conf.d")))
  178. (mkdir-p registry-dir)
  179. (when (directory-exists? deps-registry-dir)
  180. (copy-recursively deps-registry-dir registry-dir))
  181. (with-output-to-file (string-append registry-dir "/50-" name ".conf")
  182. (lambda _
  183. (format #t "~y~%" `(:tree ,source-dir))))
  184. (mkdir-p translations-dir)
  185. (when (directory-exists? deps-translations-dir)
  186. (copy-recursively deps-translations-dir translations-dir))
  187. (with-output-to-file (string-append translations-dir "/50-" name ".conf")
  188. (lambda _
  189. (format #t "~y~%" `((,source-dir :**/ :*.*.*)
  190. (,lib-dir :**/ :*.*.*)))))))
  191. (define (replace-escaped-macros string)
  192. "Replace simple lisp forms that the guile writer escapes, for example by
  193. replacing #{#p}# with #p. Should only be used to replace truly simple forms
  194. which are not nested."
  195. (regexp-substitute/global #f "(#\\{)(\\S*)(\\}#)" string
  196. 'pre 2 'post))
  197. (define (prepend-to-source-registry path)
  198. (setenv "CL_SOURCE_REGISTRY"
  199. (string-append path ":" (or (getenv "CL_SOURCE_REGISTRY") ""))))
  200. (define* (build-program program outputs #:key
  201. (dependency-prefixes (list (library-output outputs)))
  202. (dependencies (list (basename program)))
  203. entry-program
  204. compress?
  205. #:allow-other-keys)
  206. "Generate an executable program containing all DEPENDENCIES, and which will
  207. execute ENTRY-PROGRAM. The result is placed in PROGRAM. When executed, it
  208. will run ENTRY-PROGRAM, a list of Common Lisp expressions in which `arguments'
  209. has been bound to the command-line arguments which were passed. Link in any
  210. asd files from DEPENDENCY-PREFIXES to ensure references to those libraries are
  211. retained."
  212. (setenv "XDG_CONFIG_DIRS" (string-append (library-output outputs) "/etc"))
  213. (generate-executable program
  214. #:dependencies dependencies
  215. #:dependency-prefixes dependency-prefixes
  216. #:entry-program entry-program
  217. #:compress? compress?
  218. #:type 'asdf:program-op)
  219. (let* ((name (basename program))
  220. (bin-directory (dirname program)))
  221. (with-directory-excursion bin-directory
  222. (rename-file (string-append name "-exec")
  223. name)))
  224. #t)
  225. (define* (build-image image outputs #:key
  226. (dependency-prefixes (list (library-output outputs)))
  227. (dependencies (list (basename image)))
  228. #:allow-other-keys)
  229. "Generate an image, possibly standalone, which contains all DEPENDENCIES,
  230. placing the result in IMAGE.image. Link in any asd files from
  231. DEPENDENCY-PREFIXES to ensure references to those libraries are retained."
  232. (setenv "XDG_CONFIG_DIRS" (string-append (library-output outputs) "/etc"))
  233. (generate-executable image
  234. #:dependencies dependencies
  235. #:dependency-prefixes dependency-prefixes
  236. #:entry-program '(nil)
  237. #:type 'asdf:image-op)
  238. (let* ((name (basename image))
  239. (bin-directory (dirname image)))
  240. (with-directory-excursion bin-directory
  241. (rename-file (string-append name "-exec--all-systems.image")
  242. (string-append name ".image"))))
  243. #t)
  244. (define* (generate-executable out-file #:key
  245. dependencies
  246. dependency-prefixes
  247. entry-program
  248. type
  249. compress?
  250. #:allow-other-keys)
  251. "Generate an executable by using asdf operation TYPE, containing within the
  252. image all DEPENDENCIES, and running ENTRY-PROGRAM in the case of an
  253. executable. Link in any asd files from DEPENDENCY-PREFIXES to ensure
  254. references to those libraries are retained."
  255. (let* ((bin-directory (dirname out-file))
  256. (name (basename out-file)))
  257. (mkdir-p bin-directory)
  258. (with-directory-excursion bin-directory
  259. (generate-executable-wrapper-system name dependencies)
  260. (generate-executable-entry-point name entry-program)
  261. (setenv "ASDF_OUTPUT_TRANSLATIONS"
  262. (replace-escaped-macros
  263. (format
  264. #f "~S"
  265. (wrap-output-translations
  266. `(((,bin-directory :**/ :*.*.*)
  267. (,bin-directory :**/ :*.*.*)))))))
  268. (generate-executable-for-system type name #:compress? compress?))
  269. (let* ((after-store-prefix-index
  270. (string-index out-file #\/
  271. (1+ (string-length (%store-directory)))))
  272. (output (string-take out-file after-store-prefix-index))
  273. (hidden-asd-links (string-append output "/.asd-files")))
  274. (mkdir-p hidden-asd-links)
  275. (for-each
  276. (lambda (path)
  277. (for-each
  278. (lambda (asd-file)
  279. (symlink asd-file
  280. (string-append hidden-asd-links
  281. "/" (basename asd-file))))
  282. (find-files (string-append path %source-install-prefix "/"
  283. (%lisp-type))
  284. "\\.asd$")))
  285. dependency-prefixes))
  286. (delete-file (string-append bin-directory "/" name "-exec.asd"))
  287. (delete-file (string-append bin-directory "/" name "-exec.lisp"))
  288. (delete-file (string-append bin-directory "/" name "-exec.fasl"))))