1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2016, 2017 Andy Patterson <ajpatter@uwaterloo.ca>
4 ;;; This file is part of GNU Guix.
6 ;;; GNU Guix is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or (at
9 ;;; your option) any later version.
11 ;;; GNU Guix is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; GNU General Public License for more details.
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; 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)
29 %source-install-prefix
33 replace-escaped-macros
34 generate-executable-wrapper-system
35 generate-executable-entry-point
36 generate-executable-for-system
37 %bundle-install-prefix
39 wrap-output-translations
40 prepend-to-source-registry
50 ;;; Tools to evaluate lisp programs within a lisp session, generate wrapper
51 ;;; systems for executables. Compile, test, and produce images for systems and
52 ;;; programs, and link them with their dependencies.
57 ;; File name of the Lisp compiler.
58 (make-parameter "lisp"))
61 ;; String representing the class of implementation being used.
62 (make-parameter "lisp"))
64 ;; The common parent for Lisp source files, as will as the symbolic
65 ;; link farm for system definition (.asd) files.
66 (define %source-install-prefix "/share/common-lisp")
68 (define (%bundle-install-prefix)
69 (string-append %source-install-prefix "/" (%lisp-type) "-bundle-systems"))
71 (define (library-output outputs)
72 "If a `lib' output exists, build things there. Otherwise use `out'."
73 (or (assoc-ref outputs "lib") (assoc-ref outputs "out")))
75 ;; See nix/libstore/store-api.cc#checkStoreName.
76 (define valid-char-set
78 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-._?="))
80 (define (normalize-string str)
81 "Replace invalid characters in STR with a hyphen."
82 (string-join (string-tokenize str valid-char-set) "-"))
84 (define (normalize-dependency dependency)
85 "Normalize the name of DEPENDENCY. Handles dependency definitions of the
86 dependency-def form described by
87 <https://common-lisp.net/project/asdf/asdf.html#The-defsystem-grammar>.
88 Assume that any symbols in DEPENDENCY will be in upper-case."
90 ((':VERSION name rest ...)
91 `(:version ,(normalize-string name) ,@rest))
92 ((':FEATURE feature-specification dependency-specification)
94 ,feature-specification
95 ,(normalize-dependency dependency-specification)))
96 ((? string? name) (normalize-string name))
97 (require-specification require-specification)))
99 (define (inputs->asd-file-map inputs)
100 "Produce a hash table of the form (system . asd-file), where system is the
101 name of an ASD system, and asd-file is the full path to its definition."
106 (let ((prefix (string-append path (%bundle-install-prefix))))
107 (and (directory-exists? prefix)
108 (match (find-files prefix "\\.asd$")
111 (string-drop-right (basename asd-file) 4) ; drop ".asd"
116 (define (wrap-output-translations translations)
117 `(:output-translations
119 :inherit-configuration))
121 (define (lisp-eval-program program)
122 "Evaluate PROGRAM with a given LISP implementation."
123 (define invocation (lisp-invocation program))
124 (format #t "Invoking ~a: ~{~s ~}~%" (%lisp-type) invocation)
125 (apply invoke invocation))
127 (define (spread-statements program argument-name)
128 "Return a list with the statements from PROGRAM spread between
129 ARGUMENT-NAME, a string representing the argument a lisp implementation uses
130 to accept statements to be evaluated before starting."
131 (append-map (lambda (statement)
132 (list argument-name (format #f "~S" statement)))
135 (define (lisp-invocation program)
136 "Return a list of arguments for system* determining how to invoke LISP
139 ("sbcl" `(,(%lisp) "--non-interactive"
140 ,@(spread-statements program "--eval")))
142 ,@(spread-statements program "--eval")
144 (_ (error "The LISP provided is not supported at this time."))))
146 (define (asdf-load-all systems)
147 (map (lambda (system)
148 `(asdf:load-system ,system))
151 (define (compile-system system asd-file)
152 "Use a lisp implementation to compile SYSTEM using asdf. Load ASD-FILE
156 (asdf:load-asd (truename ,asd-file) :name ,(normalize-string system))
157 (asdf:operate 'asdf:compile-bundle-op ,system))))
159 (define (system-dependencies system asd-file)
160 "Return the dependencies of SYSTEM, as reported by
161 asdf:system-depends-on. First load the system's ASD-FILE."
162 (define deps-file ".deps.sexp")
165 (asdf:load-asd (truename ,asd-file) :name ,(normalize-string system))
167 (stream ,deps-file :direction :output)
170 (asdf:system-depends-on
171 (asdf:find-system ,system))))))
175 (lisp-eval-program program))
177 (call-with-input-file deps-file read))
179 (when (file-exists? deps-file)
180 (delete-file deps-file)))))
182 (define (compiled-system system)
183 (let ((system (basename system))) ; this is how asdf handles slashes
185 ("sbcl" (string-append system "--system"))
188 (define* (generate-system-definition system
189 #:key version dependencies component?)
191 ,(normalize-string system)
193 '(:class asdf/bundle:prebuilt-system)
196 :depends-on ,dependencies
198 `(:components ((:compiled-file ,(compiled-system system))))
200 ,@(if (string=? "ecl" (%lisp-type))
201 `(:lib ,(string-append system ".a"))
204 (define (test-system system asd-file test-asd-file)
205 "Use a lisp implementation to test SYSTEM using asdf. Load ASD-FILE first.
206 Also load TEST-ASD-FILE if necessary."
209 (asdf:load-asd (truename ,asd-file) :name ,(normalize-string system))
211 `((asdf:load-asd (truename ,test-asd-file)))
212 ;; Try some likely files.
214 `(when (uiop:file-exists-p ,file)
215 (asdf:load-asd (truename ,file))))
217 (string-append system "-tests.asd")
218 (string-append system "-test.asd")
221 (asdf:test-system ,system))))
223 (define (string->lisp-keyword . strings)
224 "Return a lisp keyword for the concatenation of STRINGS."
225 (string->symbol (apply string-append ":" strings)))
227 (define* (generate-executable-for-system type system #:key compress?)
228 "Use LISP to generate an executable, whose TYPE can be 'asdf:image-op or
229 'asdf:program-op. The latter will always be standalone. Depends on having
230 created a \"SYSTEM-exec\" system which contains the entry program."
233 ;; Only SBCL supports compression as of 2019-09-02.
234 ,(if (and compress? (string=? (%lisp-type) "sbcl"))
235 '(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
236 (uiop:dump-image (asdf:output-file o c)
240 (asdf:operate ',type ,(string-append system "-exec")))))
242 (define (generate-executable-wrapper-system system dependencies)
243 "Generates a system which can be used by asdf to produce an image or program
244 inside the current directory. The image or program will contain
246 (with-output-to-file (string-append system "-exec.asd")
249 `(defsystem ,(string->lisp-keyword system "-exec")
250 :entry-point ,(string-append system "-exec:main")
252 ,@(map string->lisp-keyword
254 :components ((:file ,(string-append system "-exec"))))))))
256 (define (generate-executable-entry-point system entry-program)
257 "Generates an entry point program from the list of lisp statements
258 ENTRY-PROGRAM for SYSTEM within the current directory."
259 (with-output-to-file (string-append system "-exec.lisp")
261 (let ((system (string->lisp-keyword system "-exec")))
262 (format #t "~{~y~%~%~}"
263 `((defpackage ,system
270 (let ((arguments uiop:*command-line-arguments*))
271 (declare (ignorable arguments))
272 ,@entry-program))))))))
274 (define (generate-dependency-links registry system)
275 "Creates a program which populates asdf's source registry from REGISTRY, an
276 alist of dependency names to corresponding asd files. This allows the system
277 to locate its dependent systems."
279 (asdf/source-registry:ensure-source-registry)
284 asdf/source-registry:*source-registry*)
285 ,(string->symbol "#p")
289 (define* (make-asd-file asd-file
290 #:key system version inputs
291 (system-asd-file #f))
292 "Create an ASD-FILE for SYSTEM@VERSION, appending a program to allow the
293 system to find its dependencies, as described by GENERATE-DEPENDENCY-LINKS."
296 (system-dependencies system system-asd-file)))
299 (map normalize-dependency deps))))
301 (define lisp-input-map
302 (inputs->asd-file-map inputs))
304 (define dependency-name
306 ((':version name _ ...) name)
307 ((':feature _ dependency-specification)
308 (dependency-name dependency-specification))
309 ((? string? name) name)
313 (filter-map hash-get-handle
314 (make-list (length dependencies)
316 (map dependency-name dependencies)))
318 ;; Ensure directory exists, which might not be the case for an .asd without components.
319 (mkdir-p (dirname asd-file))
320 (call-with-output-file asd-file
323 (replace-escaped-macros
324 (format #f "~y~%~y~%"
325 (generate-system-definition
328 #:dependencies dependencies
329 ;; Some .asd don't have components, and thus they don't generate any .fasl.
330 #:component? (match (%lisp-type)
331 ("sbcl" (pair? (find-files (dirname asd-file)
332 "--system\\.fasl$")))
333 ("ecl" (pair? (find-files (dirname asd-file)
335 (_ (error "The LISP provided is not supported at this time."))))
336 (generate-dependency-links registry system)))
339 (define (bundle-asd-file output-path original-asd-file)
340 "Find the symlinked bundle file for ORIGINAL-ASD-FILE by looking in
341 OUTPUT-PATH/share/common-lisp/LISP-bundle-systems/<system>.asd. Returns two
342 values: the asd file itself and the directory in which it resides."
343 (let ((bundle-asd-path (string-append output-path
344 (%bundle-install-prefix))))
345 (values (string-append bundle-asd-path "/" (basename original-asd-file))
348 (define (replace-escaped-macros string)
349 "Replace simple lisp forms that the guile writer escapes, for example by
350 replacing #{#p}# with #p. Should only be used to replace truly simple forms
351 which are not nested."
352 (regexp-substitute/global #f "(#\\{)(\\S*)(\\}#)" string
355 (define (prepend-to-source-registry path)
356 (setenv "CL_SOURCE_REGISTRY"
357 (string-append path ":" (or (getenv "CL_SOURCE_REGISTRY") ""))))
359 (define* (build-program program outputs #:key
360 (dependency-prefixes (list (library-output outputs)))
361 (dependencies (list (basename program)))
365 "Generate an executable program containing all DEPENDENCIES, and which will
366 execute ENTRY-PROGRAM. The result is placed in PROGRAM. When executed, it
367 will run ENTRY-PROGRAM, a list of Common Lisp expressions in which `arguments'
368 has been bound to the command-line arguments which were passed. Link in any
369 asd files from DEPENDENCY-PREFIXES to ensure references to those libraries are
371 (generate-executable program
372 #:dependencies dependencies
373 #:dependency-prefixes dependency-prefixes
374 #:entry-program entry-program
375 #:compress? compress?
376 #:type 'asdf:program-op)
377 (let* ((name (basename program))
378 (bin-directory (dirname program)))
379 (with-directory-excursion bin-directory
380 (rename-file (string-append name "-exec")
384 (define* (build-image image outputs #:key
385 (dependency-prefixes (list (library-output outputs)))
386 (dependencies (list (basename image)))
388 "Generate an image, possibly standalone, which contains all DEPENDENCIES,
389 placing the result in IMAGE.image. Link in any asd files from
390 DEPENDENCY-PREFIXES to ensure references to those libraries are retained."
391 (generate-executable image
392 #:dependencies dependencies
393 #:dependency-prefixes dependency-prefixes
394 #:entry-program '(nil)
395 #:type 'asdf:image-op)
396 (let* ((name (basename image))
397 (bin-directory (dirname image)))
398 (with-directory-excursion bin-directory
399 (rename-file (string-append name "-exec--all-systems.image")
400 (string-append name ".image"))))
403 (define* (generate-executable out-file #:key
410 "Generate an executable by using asdf operation TYPE, containing whithin the
411 image all DEPENDENCIES, and running ENTRY-PROGRAM in the case of an
412 executable. Link in any asd files from DEPENDENCY-PREFIXES to ensure
413 references to those libraries are retained."
414 (let* ((bin-directory (dirname out-file))
415 (name (basename out-file)))
416 (mkdir-p bin-directory)
417 (with-directory-excursion bin-directory
418 (generate-executable-wrapper-system name dependencies)
419 (generate-executable-entry-point name entry-program))
421 (prepend-to-source-registry
422 (string-append bin-directory "/"))
424 (setenv "ASDF_OUTPUT_TRANSLATIONS"
425 (replace-escaped-macros
428 (wrap-output-translations
429 `(((,bin-directory :**/ :*.*.*)
430 (,bin-directory :**/ :*.*.*)))))))
432 (generate-executable-for-system type name #:compress? compress?)
434 (let* ((after-store-prefix-index
435 (string-index out-file #\/
436 (1+ (string-length (%store-directory)))))
437 (output (string-take out-file after-store-prefix-index))
438 (hidden-asd-links (string-append output "/.asd-files")))
440 (mkdir-p hidden-asd-links)
446 (string-append hidden-asd-links
447 "/" (basename asd-file))))
448 (find-files (string-append path (%bundle-install-prefix))
450 dependency-prefixes))
452 (delete-file (string-append bin-directory "/" name "-exec.asd"))
453 (delete-file (string-append bin-directory "/" name "-exec.lisp"))))