lisp-utils.scm 13 KB

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