summary refs log tree commit diff
path: root/pkgs/development/lisp-modules/quicklisp-to-nix/ql-to-nix.lisp
blob: 2623990856e39d08490fe2a1b7ed923b4fb84266 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
(unless (find-package :ql-to-nix-util)
  (load "util.lisp"))
(unless (find-package :ql-to-nix-quicklisp-bootstrap)
  (load "quicklisp-bootstrap.lisp"))
(defpackage :ql-to-nix
  (:use :common-lisp :ql-to-nix-util :ql-to-nix-quicklisp-bootstrap))
(in-package :ql-to-nix)

;; We're going to pull in our dependencies at image dumping time in an
;; isolated quicklisp installation.  Unfortunately, that means that we
;; can't yet access the symbols for our dependencies.  We can probably
;; do better (by, say, loading these dependencies before this file),
;; but...

(defvar *required-systems* nil)

(push :cl-emb *required-systems*)
(wrap :cl-emb register-emb)
(wrap :cl-emb execute-emb)

(push :external-program *required-systems*)
(wrap :external-program run)

(push :cl-ppcre *required-systems*)
(wrap :cl-ppcre split)
(wrap :cl-ppcre regex-replace-all)
(wrap :cl-ppcre scan)

(push :alexandria *required-systems*)
(wrap :alexandria read-file-into-string)
(wrap :alexandria write-string-into-file)

(push :md5 *required-systems*)
(wrap :md5 md5sum-file)

(wrap :ql-dist find-system)
(wrap :ql-dist release)
(wrap :ql-dist provided-systems)
(wrap :ql-dist archive-url)
(wrap :ql-dist local-archive-file)
(wrap :ql-dist ensure-local-archive-file)
(wrap :ql-dist archive-md5)
(wrap :ql-dist name)
(wrap :ql-dist short-description)

(defun escape-filename (s)
  (format 
   nil "~a~{~a~}"
   (if (scan "^[a-zA-Z_]" s) "" "_")
   (loop
      for x in (map 'list 'identity s)
      collect
        (case x
          (#\/ "_slash_")
          (#\\ "_backslash_")
          (#\_ "__")
          (#\. "_dot_")
          (t x)))))

(defvar *system-info-bin*
  (let* ((path (uiop:getenv "system-info"))
         (path-dir (if (equal #\/ (aref path (1- (length path))))
                       path
                       (concatenate 'string path "/")))
         (pathname (parse-namestring path-dir)))
    (merge-pathnames #P"bin/quicklisp-to-nix-system-info" pathname))
  "The path to the quicklisp-to-nix-system-info binary.")

(defvar *cache-dir* nil
  "The folder where fasls will be cached.")

(defun raw-system-info (system-name)
  "Run quicklisp-to-nix-system-info on the given system and return the
form produced by the program."
  (when *cache-dir*
    (let ((command `(,*system-info-bin* "--cacheDir" ,(namestring *cache-dir*) ,system-name)))
      (handler-case
          (return-from raw-system-info
            (read (make-string-input-stream (uiop:run-program command :output :string))))
        (error (e)
          ;; Some systems don't like the funky caching that we're
          ;; doing.  That's okay.  Let's try it uncached before we
          ;; give up.
          (warn "Unable to use cache for system ~A.~%~A" system-name e)))))
  (read (make-string-input-stream (uiop:run-program `(,*system-info-bin* ,system-name) :output :string))))

(defvar *system-data-memoization-path* nil
  "The path to the folder where fully-resolved system information can
be cached.

If information for a system is found in this directory, `system-data'
will use it instead of re-computing the system data.")

(defvar *system-data-in-memory-memoization*
  (make-hash-table :test #'equalp))

(defun memoized-system-data-path (system)
  "Return the path to the file that (if it exists) contains
pre-computed system data."
  (when *system-data-memoization-path*
    (merge-pathnames (make-pathname :name system :type "txt") *system-data-memoization-path*)))

(defun memoized-system-data (system)
  "Attempts to locate memoized system data in the path specified by
`*system-data-memoization-path*'."
  (multiple-value-bind (value found) (gethash system *system-data-in-memory-memoization*)
    (when found
      (return-from memoized-system-data (values value found))))
  (let ((path (memoized-system-data-path system)))
    (unless path
      (return-from memoized-system-data (values nil nil)))
    (with-open-file (s path :if-does-not-exist nil :direction :input)
      (unless s
        (return-from memoized-system-data (values nil nil)))
      (return-from memoized-system-data (values (read s) t)))))

(defun set-memoized-system-data (system data)
  "Store system data in the path specified by
`*system-data-memoization-path*'."
  (setf (gethash system *system-data-in-memory-memoization*) data)
  (let ((path (memoized-system-data-path system)))
    (unless path
      (return-from set-memoized-system-data data))
    (with-open-file (s path :direction :output :if-exists :supersede)
      (format s "~W" data)))
  data)

(defun system-data (system)
  "Examine a quicklisp system name and figure out everything that is
required to produce a nix package.

This function stores results for memoization purposes in files within
`*system-data-memoization-path*'."
  (multiple-value-bind (value found) (memoized-system-data system)
    (when found
      (return-from system-data value)))
  (format t "Examining system ~A~%" system)
  (let* ((system-info (raw-system-info system))
         (host (getf system-info :host))
         (host-name (getf system-info :host-name))
         (name (getf system-info :name)))
    (when host
      (return-from system-data
        (set-memoized-system-data
         system
         (list
          :system (getf system-info :system)
          :host host
          :filename (escape-filename name)
          :host-filename (escape-filename host-name)))))

    (let* ((url (getf system-info :url))
           (sha256 (getf system-info :sha256))
           (archive-data (nix-prefetch-url url :expected-sha256 sha256))
           (archive-path (getf archive-data :path))
           (archive-md5 (string-downcase
                         (format nil "~{~16,2,'0r~}"
                                 (map 'list 'identity (md5sum-file archive-path)))))
           (stated-md5 (getf system-info :md5))
           (dependencies (getf system-info :dependencies))
           (deps (mapcar (lambda (x) (list :name x :filename (escape-filename x)))
                         dependencies))
           (description (getf system-info :description))
           (siblings (getf system-info :siblings))
           (release-name (getf system-info :release-name))
           (parasites (getf system-info :parasites))
           (version (regex-replace-all
                     (format nil "~a-" name) release-name "")))
      (assert (equal archive-md5 stated-md5))
      (set-memoized-system-data
       system
       (list
        :system system
        :description description
        :sha256 sha256
        :url url
        :md5 stated-md5
        :name name
        :filename (escape-filename name)
        :deps deps
        :dependencies dependencies
        :version version
        :siblings siblings
        :parasites parasites)))))

(defun parasitic-p (data)
  (getf data :host))

(defvar *loaded-from* (or *compile-file-truename* *load-truename*)
  "Where this source file is located.")

(defun this-file ()
  "Where this source file is located or an error."
  (or *loaded-from* (error "Not sure where this file is located!")))

(defun nix-expression (system)
  (execute-emb
    "nix-package"
    :env (system-data system)))

(defun nix-invocation (system)
  (let ((data (system-data system)))
    (if (parasitic-p data)
        (execute-emb
         "parasitic-invocation"
         :env data)
        (execute-emb
         "invocation"
         :env data))))

(defun systems-closure (systems)
  (let*
    ((seen (make-hash-table :test 'equal)))
    (loop
      with queue := systems
      with res := nil
      while queue
      for next := (pop queue)
      for old := (gethash next seen)
      for data := (unless old (system-data next))
      for deps := (getf data :dependencies)
      for siblings := (getf data :siblings)
      unless old do
      (progn
        (push next res)
        (setf queue (append queue deps)))
      do (setf (gethash next seen) t)
      finally (return res))))

(defun ql-to-nix (target-directory)
  (let*
    ((systems
       (split
         (format nil "~%")
         (read-file-into-string
          (format nil "~a/quicklisp-to-nix-systems.txt" target-directory))))
     (closure (systems-closure systems))
     (invocations
       (loop for s in closure
             collect (list :code (nix-invocation s)))))
    (loop
      for s in closure
       do (unless (parasitic-p (system-data s))
            (write-string-into-file
             (nix-expression s)
             (format nil "~a/quicklisp-to-nix-output/~a.nix"
                     target-directory (escape-filename s))
             :if-exists :supersede)))
    (write-string-into-file
      (execute-emb
        "top-package"
        :env (list :invocations invocations))
      (format nil "~a/quicklisp-to-nix.nix" target-directory)
      :if-exists :supersede)))

(defun print-usage-and-quit ()
  "Does what it says on the tin."
  (format *error-output* "Usage:
    ~A [--help] [--cacheSystemInfoDir <path>] <work-dir>
Arguments:
    --cacheSystemInfoDir Store computed system info in the given directory
    --help Print usage and exit
    <work-dir> Path to directory with quicklisp-to-nix-systems.txt
" (uiop:argv0))
  (uiop:quit 2))

(defun main ()
  "Make it go"
  (let ((argv (uiop:command-line-arguments))
        work-directory
        cache-system-info-directory
        cache-fasl-directory)
    (loop :while argv :for arg = (pop argv) :do
       (cond
         ((equal arg "--cacheSystemInfoDir")
          (unless argv
            (format *error-output* "--cacheSystemInfoDir requires an argument~%")
            (print-usage-and-quit))
          (setf cache-system-info-directory (pop argv)))

         ((equal arg "--cacheFaslDir")
          (unless argv
            (format *error-output* "--cacheFaslDir requires an argument~%")
            (print-usage-and-quit))
          (setf cache-fasl-directory (pop argv)))

         ((equal arg "--help")
          (print-usage-and-quit))

         (t
          (when argv
            (format *error-output* "Only one positional argument allowed~%")
            (print-usage-and-quit))
          (setf work-directory arg))))

    (when cache-system-info-directory
      (setf cache-system-info-directory (pathname-as-directory (pathname cache-system-info-directory)))
      (ensure-directories-exist cache-system-info-directory))

    (labels
        ((make-go (*cache-dir*)
           (format t "Caching fasl files in ~A~%" *cache-dir*)

           (let ((*system-data-memoization-path* cache-system-info-directory))
             (ql-to-nix work-directory))))
      (if cache-fasl-directory
          (make-go (truename (pathname-as-directory (parse-namestring (ensure-directories-exist cache-fasl-directory)))))
          (with-temporary-directory (*cache-dir*)
            (make-go *cache-dir*))))))

(defun dump-image ()
  "Make an executable"
  (dolist (system *required-systems*)
    (asdf:make system))
  (register-emb "nix-package" (merge-pathnames #p"nix-package.emb" (this-file)))
  (register-emb "invocation" (merge-pathnames #p"invocation.emb" (this-file)))
  (register-emb "parasitic-invocation" (merge-pathnames #p"parasitic-invocation.emb" (this-file)))
  (register-emb "top-package" (merge-pathnames #p"top-package.emb" (this-file)))
  (setf uiop:*image-entry-point* #'main)
  (setf uiop:*lisp-interaction* nil)
  (setf *loaded-from* nil) ;; Break the link to our source
  (uiop:dump-image "quicklisp-to-nix" :executable t))