123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2016, 2017 Andy Patterson <ajpatter@uwaterloo.ca>
- ;;; Copyright © 2020, 2022 Guillaume Le Vaillant <glv@posteo.net>
- ;;; Copyright © 2022 Pierre Neidhardt <mail@ambrevar.xyz>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix is free software; you can redistribute it and/or modify it
- ;;; under the terms of the GNU General Public License as published by
- ;;; the Free Software Foundation; either version 3 of the License, or (at
- ;;; your option) any later version.
- ;;;
- ;;; GNU Guix is distributed in the hope that it will be useful, but
- ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;;; GNU General Public License for more details.
- ;;;
- ;;; You should have received a copy of the GNU General Public License
- ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (guix build lisp-utils)
- #:use-module (ice-9 format)
- #:use-module (ice-9 hash-table)
- #:use-module (ice-9 match)
- #:use-module (ice-9 regex)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-26)
- #:use-module (guix build utils)
- #:export (%lisp
- %lisp-type
- %source-install-prefix
- lisp-eval-program
- compile-systems
- test-system
- replace-escaped-macros
- generate-executable-wrapper-system
- generate-executable-entry-point
- generate-executable-for-system
- wrap-output-translations
- prepend-to-source-registry
- build-program
- build-image
- make-asdf-configuration
- valid-char-set
- normalize-string
- library-output))
- ;;; Commentary:
- ;;;
- ;;; Tools to evaluate lisp programs within a lisp session, generate wrapper
- ;;; systems for executables. Compile, test, and produce images for systems and
- ;;; programs, and link them with their dependencies.
- ;;;
- ;;; Code:
- (define %lisp
- ;; File name of the Lisp compiler.
- (make-parameter "lisp"))
- (define %lisp-type
- ;; String representing the class of implementation being used.
- (make-parameter "lisp"))
- ;; The common parent for Lisp source files, as will as the symbolic
- ;; link farm for system definition (.asd) files.
- (define %source-install-prefix "/share/common-lisp")
- (define (library-output outputs)
- "If a `lib' output exists, build things there. Otherwise use `out'."
- (or (assoc-ref outputs "lib") (assoc-ref outputs "out")))
- ;; See nix/libstore/store-api.cc#checkStoreName.
- (define valid-char-set
- (string->char-set
- "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-._?="))
- (define (normalize-string str)
- "Replace invalid characters in STR with a hyphen."
- (string-join (string-tokenize str valid-char-set) "-"))
- (define (wrap-output-translations translations)
- `(:output-translations
- ,@translations
- :inherit-configuration))
- (define (lisp-eval-program program)
- "Evaluate PROGRAM with a given LISP implementation."
- (define invocation (lisp-invocation program))
- (format #t "Invoking ~a: ~{~s ~}~%" (%lisp-type) invocation)
- (apply invoke invocation))
- (define (spread-statements program argument-name)
- "Return a list with the statements from PROGRAM spread between
- ARGUMENT-NAME, a string representing the argument a lisp implementation uses
- to accept statements to be evaluated before starting."
- (append-map (lambda (statement)
- (list argument-name (format #f "~S" statement)))
- program))
- (define (lisp-invocation program)
- "Return a list of arguments for system* determining how to invoke LISP
- with PROGRAM."
- (match (%lisp-type)
- ("sbcl" `(,(%lisp) "--non-interactive"
- ,@(spread-statements program "--eval")))
- ("ecl" `(,(%lisp)
- ,@(spread-statements program "--eval")
- "--eval" "(quit)"))
- (_ (error "The LISP provided is not supported at this time."))))
- (define (compile-systems systems directory operation)
- "Use a lisp implementation to compile the SYSTEMS using asdf."
- (lisp-eval-program
- `((require :asdf)
- (asdf:initialize-source-registry
- (list :source-registry (list :tree (uiop:ensure-pathname ,directory
- :truenamize t
- :ensure-directory t))
- :inherit-configuration))
- ,@(map (lambda (system)
- (list (string->symbol (string-append "asdf:" operation)) system))
- systems))))
- (define (test-system test-systems directory)
- "Use a lisp implementation to test the TEST-SYSTEMS using asdf."
- (lisp-eval-program
- `((require :asdf)
- (asdf:initialize-source-registry
- (list :source-registry (list :tree (uiop:ensure-pathname ,directory
- :truenamize t
- :ensure-directory t))
- :inherit-configuration))
- ,@(map (lambda (system)
- `(asdf:test-system ,system))
- test-systems))))
- (define (string->lisp-keyword . strings)
- "Return a lisp keyword for the concatenation of STRINGS."
- (string->symbol (apply string-append ":" strings)))
- (define* (generate-executable-for-system type system #:key compress?)
- "Use LISP to generate an executable, whose TYPE can be 'asdf:image-op or
- 'asdf:program-op. The latter will always be standalone. Depends on having
- created a \"SYSTEM-exec\" system which contains the entry program."
- (lisp-eval-program
- `((require :asdf)
- ;; Only SBCL supports compression as of 2019-09-02.
- ,(if (and compress? (string=? (%lisp-type) "sbcl"))
- '(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
- (uiop:dump-image (asdf:output-file o c)
- :executable t
- :compression t))
- '())
- (asdf:load-asd (truename ,(string-append system "-exec.asd")))
- (asdf:operate ',type ,(string-append system "-exec")))))
- (define (generate-executable-wrapper-system system dependencies)
- "Generates a system which can be used by asdf to produce an image or program
- inside the current directory. The image or program will contain
- DEPENDENCIES."
- (with-output-to-file (string-append system "-exec.asd")
- (lambda _
- (format #t "~y~%"
- `(defsystem ,(string->lisp-keyword system "-exec")
- :entry-point ,(string-append system "-exec:main")
- :depends-on (:uiop
- ,@(map string->lisp-keyword
- dependencies))
- :components ((:file ,(string-append system "-exec"))))))))
- (define (generate-executable-entry-point system entry-program)
- "Generates an entry point program from the list of lisp statements
- ENTRY-PROGRAM for SYSTEM within the current directory."
- (with-output-to-file (string-append system "-exec.lisp")
- (lambda _
- (let ((system (string->lisp-keyword system "-exec")))
- (format #t "~{~y~%~%~}"
- `((defpackage ,system
- (:use :cl)
- (:export :main))
- (in-package ,system)
- (defun main ()
- (let ((arguments uiop:*command-line-arguments*))
- (declare (ignorable arguments))
- ,@entry-program))))))))
- (define (make-asdf-configuration name conf-dir deps-conf-dir source-dir lib-dir)
- (let ((registry-dir (string-append
- conf-dir "/source-registry.conf.d"))
- (translations-dir (string-append
- conf-dir "/asdf-output-translations.conf.d"))
- (deps-registry-dir (string-append
- deps-conf-dir "/source-registry.conf.d"))
- (deps-translations-dir (string-append
- deps-conf-dir
- "/asdf-output-translations.conf.d")))
- (mkdir-p registry-dir)
- (when (directory-exists? deps-registry-dir)
- (copy-recursively deps-registry-dir registry-dir))
- (with-output-to-file (string-append registry-dir "/50-" name ".conf")
- (lambda _
- (format #t "~y~%" `(:tree ,source-dir))))
- (mkdir-p translations-dir)
- (when (directory-exists? deps-translations-dir)
- (copy-recursively deps-translations-dir translations-dir))
- (with-output-to-file (string-append translations-dir "/50-" name ".conf")
- (lambda _
- (format #t "~y~%" `((,source-dir :**/ :*.*.*)
- (,lib-dir :**/ :*.*.*)))))))
- (define (replace-escaped-macros string)
- "Replace simple lisp forms that the guile writer escapes, for example by
- replacing #{#p}# with #p. Should only be used to replace truly simple forms
- which are not nested."
- (regexp-substitute/global #f "(#\\{)(\\S*)(\\}#)" string
- 'pre 2 'post))
- (define (prepend-to-source-registry path)
- (setenv "CL_SOURCE_REGISTRY"
- (string-append path ":" (or (getenv "CL_SOURCE_REGISTRY") ""))))
- (define* (build-program program outputs #:key
- (dependency-prefixes (list (library-output outputs)))
- (dependencies (list (basename program)))
- entry-program
- compress?
- #:allow-other-keys)
- "Generate an executable program containing all DEPENDENCIES, and which will
- execute ENTRY-PROGRAM. The result is placed in PROGRAM. When executed, it
- will run ENTRY-PROGRAM, a list of Common Lisp expressions in which `arguments'
- has been bound to the command-line arguments which were passed. Link in any
- asd files from DEPENDENCY-PREFIXES to ensure references to those libraries are
- retained."
- (setenv "XDG_CONFIG_DIRS" (string-append (library-output outputs) "/etc"))
- (generate-executable program
- #:dependencies dependencies
- #:dependency-prefixes dependency-prefixes
- #:entry-program entry-program
- #:compress? compress?
- #:type 'asdf:program-op)
- (let* ((name (basename program))
- (bin-directory (dirname program)))
- (with-directory-excursion bin-directory
- (rename-file (string-append name "-exec")
- name)))
- #t)
- (define* (build-image image outputs #:key
- (dependency-prefixes (list (library-output outputs)))
- (dependencies (list (basename image)))
- #:allow-other-keys)
- "Generate an image, possibly standalone, which contains all DEPENDENCIES,
- placing the result in IMAGE.image. Link in any asd files from
- DEPENDENCY-PREFIXES to ensure references to those libraries are retained."
- (setenv "XDG_CONFIG_DIRS" (string-append (library-output outputs) "/etc"))
- (generate-executable image
- #:dependencies dependencies
- #:dependency-prefixes dependency-prefixes
- #:entry-program '(nil)
- #:type 'asdf:image-op)
- (let* ((name (basename image))
- (bin-directory (dirname image)))
- (with-directory-excursion bin-directory
- (rename-file (string-append name "-exec--all-systems.image")
- (string-append name ".image"))))
- #t)
- (define* (generate-executable out-file #:key
- dependencies
- dependency-prefixes
- entry-program
- type
- compress?
- #:allow-other-keys)
- "Generate an executable by using asdf operation TYPE, containing within the
- image all DEPENDENCIES, and running ENTRY-PROGRAM in the case of an
- executable. Link in any asd files from DEPENDENCY-PREFIXES to ensure
- references to those libraries are retained."
- (let* ((bin-directory (dirname out-file))
- (name (basename out-file)))
- (mkdir-p bin-directory)
- (with-directory-excursion bin-directory
- (generate-executable-wrapper-system name dependencies)
- (generate-executable-entry-point name entry-program)
- (setenv "ASDF_OUTPUT_TRANSLATIONS"
- (replace-escaped-macros
- (format
- #f "~S"
- (wrap-output-translations
- `(((,bin-directory :**/ :*.*.*)
- (,bin-directory :**/ :*.*.*)))))))
- (generate-executable-for-system type name #:compress? compress?))
- (let* ((after-store-prefix-index
- (string-index out-file #\/
- (1+ (string-length (%store-directory)))))
- (output (string-take out-file after-store-prefix-index))
- (hidden-asd-links (string-append output "/.asd-files")))
- (mkdir-p hidden-asd-links)
- (for-each
- (lambda (path)
- (for-each
- (lambda (asd-file)
- (symlink asd-file
- (string-append hidden-asd-links
- "/" (basename asd-file))))
- (find-files (string-append path %source-install-prefix "/"
- (%lisp-type))
- "\\.asd$")))
- dependency-prefixes))
- (delete-file (string-append bin-directory "/" name "-exec.asd"))
- (delete-file (string-append bin-directory "/" name "-exec.lisp"))
- (delete-file (string-append bin-directory "/" name "-exec.fasl"))))
|