1 (unless (find-package :ql-to-nix-util
)
3 (unless (find-package :ql-to-nix-quicklisp-bootstrap
)
4 (load "quicklisp-bootstrap.lisp"))
5 (defpackage :ql-to-nix-system-info
6 (:use
:common-lisp
:ql-to-nix-quicklisp-bootstrap
:ql-to-nix-util
)
7 (:export
#:dump-image
))
8 (in-package :ql-to-nix-system-info
)
10 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
11 (defparameter *implementation-systems
*
13 #+sbcl
(list :sb-posix
:sb-bsd-sockets
:sb-rotate-byte
:sb-cltl2
14 :sb-introspect
:sb-rt
:sb-concurrency
)))
15 (mapcar (function require
) *implementation-systems
*))
17 (declaim (optimize (debug 3) (speed 0) (space 0) (compilation-speed 0) (safety 3)))
19 ;; This file cannot have any dependencies beyond quicklisp and asdf.
20 ;; Otherwise, we'll miss some dependencies!
22 ;; (Implementation-provided dependencies are special, though)
24 ;; We can't load quicklisp until runtime (at which point we'll create
25 ;; an isolated quicklisp installation). These wrapper functions are
26 ;; nicer than funcalling intern'd symbols every time we want to talk
28 (wrap :ql apply-load-strategy
)
29 (wrap :ql compute-load-strategy
)
30 (wrap :ql show-load-strategy
)
31 (wrap :ql quicklisp-systems
)
32 (wrap :ql ensure-installed
)
33 (wrap :ql quicklisp-releases
)
34 (wrap :ql-dist archive-md5
)
35 (wrap :ql-dist archive-url
)
36 (wrap :ql-dist ensure-local-archive-file
)
37 (wrap :ql-dist find-system
)
38 (wrap :ql-dist local-archive-file
)
40 (wrap :ql-dist provided-systems
)
41 (wrap :ql-dist release
)
42 (wrap :ql-dist short-description
)
43 (wrap :ql-dist system-file-name
)
44 (wrap :ql-impl-util call-with-quiet-compilation
)
46 (defvar *version
* (uiop:getenv
"version")
47 "The version number of this program")
49 (defvar *main-system
* nil
50 "The name of the system we're trying to extract info from.")
52 (defvar *found-parasites
* (make-hash-table :test
#'equalp
)
53 "Names of systems which have been identified as parasites.
55 A system is parasitic if its name doesn't match the name of the file
56 it is defined in. So, for example, if foo and foo-bar are both
57 defined in a file named foo.asd, foo would be the host system and
58 foo-bar would be a parasitic system.
60 Parasitic systems are not generally loaded without loading the host
63 Keys are system names. Values are unspecified.")
65 (defvar *found-dependencies
* (make-hash-table :test
#'equalp
)
66 "Hash table containing the set of dependencies discovered while installing a system.
68 Keys are system names. Values are unspecified.")
70 (defun decode-asdf-dependency (name)
71 "Translates an asdf system dependency description into a system name.
73 For example, translates (:version :foo \"1.0\") into \"foo\"."
76 (setf name
(symbol-name name
)))
81 (warn "Discarding version information ~A" name
)
82 ;; There's nothing we can do about this. If the version we
83 ;; have around is good enough, then we're golden. If it isn't
84 ;; good enough, then we'll error out and let a human figure it
86 (setf name
(second name
))
87 (return-from decode-asdf-dependency
88 (decode-asdf-dependency name
)))
91 (if (find (second name
) *features
*)
92 (return-from decode-asdf-dependency
93 (decode-asdf-dependency (third name
)))
95 (warn "Dropping dependency due to missing feature: ~A" name
)
96 (return-from decode-asdf-dependency nil
))))
99 ;; This probably isn't a dependency we can satisfy using
100 ;; quicklisp, but we might as well try anyway.
101 (return-from decode-asdf-dependency
102 (decode-asdf-dependency (second name
)))))))
103 (string-downcase name
))
105 (defun found-new-parasite (system-name)
106 "Record that the given system has been identified as a parasite."
107 (setf system-name
(decode-asdf-dependency system-name
))
108 (setf (gethash system-name
*found-parasites
*) t
)
109 (when (nth-value 1 (gethash system-name
*found-dependencies
*))
110 (error "Found dependency on parasite")))
112 (defun known-parasite-p (system-name)
113 "Have we previously identified this system as a parasite?"
114 (nth-value 1 (gethash system-name
*found-parasites
*)))
116 (defun found-parasites ()
117 "Return a vector containing all identified parasites."
118 (let ((systems (make-array (hash-table-size *found-parasites
*) :fill-pointer
0)))
119 (loop :for system
:being
:the
:hash-keys
:of
*found-parasites
* :do
120 (vector-push system systems
))
123 (defvar *track-dependencies
* nil
124 "When this variable is nil, found-new-dependency will not record
127 (defun parasitic-relationship-p (potential-host potential-parasite
)
128 "Returns t if potential-host and potential-parasite have a parasitic relationship.
130 See `*found-parasites*'."
131 (let ((host-ql-system (find-system potential-host
))
132 (parasite-ql-system (find-system potential-parasite
)))
133 (and host-ql-system parasite-ql-system
134 (not (equal (name host-ql-system
)
135 (name parasite-ql-system
)))
136 (equal (system-file-name host-ql-system
)
137 (system-file-name parasite-ql-system
)))))
139 (defun found-new-dependency (name)
140 "Record that the given system has been identified as a dependency.
142 The named system may not be recorded as a dependency. It may be left
143 out for any number of reasons. For example, if `*track-dependencies*'
144 is nil then this function does nothing. If the named system isn't a
145 quicklisp system, this function does nothing."
146 (setf name
(decode-asdf-dependency name
))
148 (return-from found-new-dependency
))
149 (unless *track-dependencies
*
150 (return-from found-new-dependency
))
151 (when (known-parasite-p name
)
152 (return-from found-new-dependency
))
153 (when (parasitic-relationship-p *main-system
* name
)
154 (found-new-parasite name
)
155 (return-from found-new-dependency
))
156 (unless (find-system name
)
157 (return-from found-new-dependency
))
158 (setf (gethash name
*found-dependencies
*) t
))
160 (defun forget-dependency (name)
161 "Whoops. Did I say that was a dependency? My bad.
163 Be very careful using this function! You can remove a system from the
164 dependency list, but you can't remove other effects associated with
165 this system. For example, transitive dependencies might still be in
166 the dependency list."
167 (setf name
(decode-asdf-dependency name
))
168 (remhash name
*found-dependencies
*))
170 (defun found-dependencies ()
171 "Return a vector containing all identified dependencies."
172 (let ((systems (make-array (hash-table-size *found-dependencies
*) :fill-pointer
0)))
173 (loop :for system
:being
:the
:hash-keys
:of
*found-dependencies
* :do
174 (vector-push system systems
))
177 (defun host-system (system-name)
178 "If the given system is a parasite, return the name of the system that is its host.
180 See `*found-parasites*'."
181 (let* ((system (find-system system-name
))
182 (host-file (system-file-name system
)))
183 (unless (equalp host-file system-name
)
186 (defun get-loaded (system)
187 "Try to load the named system using quicklisp and record any
188 dependencies quicklisp is aware of.
190 Unlike `our-quickload', this function doesn't attempt to install
191 missing dependencies."
192 ;; Let's get this party started!
193 (let* ((strategy (compute-load-strategy system
))
194 (ql-systems (quicklisp-systems strategy
)))
195 (dolist (dep ql-systems
)
196 (found-new-dependency (name dep
)))
197 (show-load-strategy strategy
)
200 (apply-load-strategy strategy
)))
201 (call-with-quiet-compilation #'make-go
)
202 (let ((asdf-system (asdf:find-system system
)))
203 ;; If ASDF says that it needed a system, then we should
204 ;; probably track that.
205 (dolist (asdf-dep (asdf:component-sideway-dependencies asdf-system
))
206 (found-new-dependency asdf-dep
))
207 (dolist (asdf-dep (asdf:system-defsystem-depends-on asdf-system
))
208 (found-new-dependency asdf-dep
))))))
210 (defun our-quickload (system)
211 "Attempt to install a package like quicklisp would, but record any
212 dependencies that are detected during the install."
213 (setf system
(string-downcase system
))
214 ;; Load it quickly, but do it OUR way. Turns out our way is very
215 ;; similar to the quicklisp way...
216 (let ((already-tried (make-hash-table :test
#'equalp
))) ;; Case insensitive
221 (asdf/find-component
:missing-dependency
(e)
222 (let ((required-by (asdf/find-component
:missing-required-by e
))
223 (missing (asdf/find-component
:missing-requires e
)))
224 (unless (typep required-by
'asdf
:system
)
226 (when (gethash missing already-tried
)
227 (error "Dependency loop? ~A" missing
))
228 (setf (gethash missing already-tried
) t
)
229 (let ((parasitic-p (parasitic-relationship-p *main-system
* missing
)))
231 (found-new-parasite missing
)
232 (found-new-dependency missing
))
233 ;; We always want to track the dependencies of systems
234 ;; that share an asd file with the main system. The
235 ;; whole asd file should be loadable. Otherwise, we
236 ;; don't want to include transitive dependencies.
237 (let ((*track-dependencies
* parasitic-p
))
238 (our-quickload missing
)))
239 (format t
"Attempting to load ~A again~%" system
)
242 (defvar *blacklisted-parasites
*
243 #("hu.dwim.stefil/documentation" ;; This system depends on :hu.dwim.stefil.test, but it should depend on hu.dwim.stefil/test
244 "named-readtables/doc" ;; Dependency cycle between named-readtabes and mgl-pax
245 "symbol-munger-test" ;; Dependency cycle between lisp-unit2 and symbol-munger
246 "cl-postgres-simple-date-tests" ;; Dependency cycle between cl-postgres and simple-date
247 "cl-containers/with-variates" ;; Symbol conflict between cl-variates:next-element, metabang.utilities:next-element
248 "serapeum/docs" ;; Weird issue with FUN-INFO redefinition
249 "spinneret/cl-markdown" ;; Weird issue with FUN-INFO redefinition
250 "spinneret/ps" ;; Weird issue with FUN-INFO redefinition
251 "spinneret/tests") ;; Weird issue with FUN-INFO redefinition
252 "A vector of systems that shouldn't be loaded by `quickload-parasitic-systems'.
254 These systems are known to be troublemakers. In some sense, all
255 parasites are troublemakers (you shouldn't define parasitic systems!).
256 However, these systems prevent us from generating nix packages and are
259 (defvar *blacklisted-parasites-table
*
260 (let ((ht (make-hash-table :test
#'equalp
)))
261 (loop :for system
:across
*blacklisted-parasites
* :do
262 (setf (gethash system ht
) t
))
264 "A hash table where each entry in `*blacklisted-parasites*' is an
265 entry in the table.")
267 (defun blacklisted-parasite-p (system-name)
268 "Returns non-nil if the named system is blacklisted"
269 (nth-value 1 (gethash system-name
*blacklisted-parasites-table
*)))
271 (defun quickload-parasitic-systems (system)
272 "Attempt to load all the systems defined in the same asd as the named system.
274 Blacklisted systems are skipped. Dependencies of the identified
275 parasitic systems will be tracked."
276 (let* ((asdf-system (asdf:find-system system
))
277 (source-file (asdf:system-source-file asdf-system
)))
280 (loop :for system-name
:being
:the
:hash-keys
:of asdf
/find-system
::*registered-systems
* :do
281 ; for an unclear reason, a literal 0 which is not a key in the hash table gets observed
282 (when (and (gethash system-name asdf
/find-system
::*registered-systems
*)
283 (parasitic-relationship-p system system-name
)
284 (not (blacklisted-parasite-p system-name
)))
285 (found-new-parasite system-name
)
286 (let ((*track-dependencies
* t
))
287 (our-quickload system-name
)))))
289 (unless (or (equal "uiop" system
)
290 (equal "asdf" system
))
291 (warn "No source file for system ~A. Can't identify parasites." system
))))))
293 (defun determine-dependencies (system)
294 "Load the named system and return a sorted vector containing all the
295 quicklisp systems that were loaded to satisfy dependencies.
297 This function should probably only be called once per process!
298 Subsequent calls will miss dependencies identified by earlier calls."
302 (let ((*standard-output
* (make-broadcast-stream))
303 (*trace-output
* (make-broadcast-stream))
304 (*main-system
* system
)
305 (*track-dependencies
* t
))
306 (our-quickload system
)
307 (quickload-parasitic-systems system
))
309 :report
"Start the quickload over again"
312 :report
"Just give up and die"
315 ;; Systems can't depend on themselves!
316 (forget-dependency system
)
319 (defun parasitic-system-data (parasite-system)
320 "Return a plist of information about the given known-parastic system.
322 Sometimes we are asked to provide information about a system that is
323 actually a parasite. The only correct response is to point them
324 toward the host system. The nix package for the host system should
325 have all the dependencies for this parasite already recorded.
327 The plist is only meant to be consumed by other parts of
329 (let ((host-system (host-system parasite-system
)))
331 :system parasite-system
333 :name
(string-downcase (format nil
"~a" parasite-system
))
334 :host-name
(string-downcase (format nil
"~a" host-system
)))))
336 (defun system-data (system)
337 "Produce a plist describing a system.
339 The plist is only meant to be consumed by other parts of
341 (when (host-system system
)
342 (return-from system-data
343 (parasitic-system-data system
)))
345 (determine-dependencies system
)
347 ((dependencies (sort (found-dependencies) #'string
<))
348 (parasites (coerce (sort (found-parasites) #'string
<) 'list
))
349 (ql-system (find-system system
))
350 (ql-release (release ql-system
))
351 (ql-sibling-systems (provided-systems ql-release
))
352 (url (archive-url ql-release
))
353 (local-archive (local-archive-file ql-release
))
354 (local-url (format nil
"file://~a" (pathname local-archive
)))
357 (ensure-local-archive-file ql-release
)
358 ;; Stuff this archive into the nix store. It was almost
359 ;; certainly going to end up there anyway (since it will
360 ;; probably be fetchurl'd for a nix package). Also, putting
361 ;; it into the store also gives us the SHA we need.
362 (nix-prefetch-url local-url
)))
363 (ideal-md5 (archive-md5 ql-release
))
364 (raw-dependencies (coerce dependencies
'list
))
365 (name (string-downcase (format nil
"~a" system
)))
367 (remove name
(mapcar 'name ql-sibling-systems
)
369 (dependencies raw-dependencies
)
372 (ignore-errors (asdf:system-description
(asdf:find-system system
)))
373 "System lacks description"))
374 (release-name (short-description ql-release
)))
377 :description description
378 :sha256
(getf archive-data
:sha256
)
382 :dependencies dependencies
383 :siblings ql-sibling-names
384 :release-name release-name
385 :parasites parasites
)))
387 (defvar *error-escape-valve
* *error-output
*
388 "When `*error-output*' is rebound to inhibit spew, this stream will
389 still produce output.")
391 (defun print-usage-and-quit ()
392 "Describe how to use this program... and then exit."
393 (format *error-output
* "Usage:
394 ~A [--cacheDir <dir>] [--silent] [--debug] [--help|-h] <system-name>
396 --cacheDir Store (and look for) compiled lisp files in the given directory
397 --verbose Show compilation output
398 --debug Enter the debugger when a fatal error is encountered
399 --help Print usage and exit
400 <system-name> The quicklisp system to examine
401 " (or (uiop:argv0
) "quicklisp-to-nix-system-info"))
406 (let ((argv (uiop:command-line-arguments
))
414 (format *error-escape-valve
* "~A~%" w
)))
420 (format *error-escape-valve
* "~
421 Failed to extract system info. Details are below. ~
422 Run with --debug and/or --verbose for more info.
425 (loop :while argv
:do
427 ((equal "--cacheDir" (first argv
))
430 (error "--cacheDir expects an argument"))
431 (setf cache-dir
(first argv
))
434 ((equal "--verbose" (first argv
))
438 ((equal "--debug" (first argv
))
442 ((or (equal "--help" (first argv
))
443 (equal "-h" (first argv
)))
444 (print-usage-and-quit))
447 (setf target-system
(pop argv
))
449 (error "Can only operate on one system")))))
451 (unless target-system
452 (print-usage-and-quit))
455 (setf cache-dir
(pathname-as-directory (parse-namestring cache-dir
))))
457 (mapcar (function require
) *implementation-systems
*)
459 (with-quicklisp (dir) (:cache-dir
(or cache-dir
:temp
))
460 (declare (ignore dir
))
463 (let ((*error-output
* (if verbose-p
465 (make-broadcast-stream)))
466 (*standard-output
* (if verbose-p
468 (make-broadcast-stream)))
469 (*trace-output
* (if verbose-p
471 (make-broadcast-stream))))
472 (format *error-output
*
473 "quicklisp-to-nix-system-info ~A~%ASDF ~A~%Quicklisp ~A~%Compiler ~A ~A~%"
476 (funcall (intern "CLIENT-VERSION" :ql
))
477 (lisp-implementation-type)
478 (lisp-implementation-version))
479 (setf system-data
(system-data target-system
)))
483 (format t
"~W~%" system-data
)
486 (format *error-output
* "Failed to determine system data~%")
491 (setf uiop
:*image-entry-point
* #'main
)
492 (setf uiop
:*lisp-interaction
* nil
)
493 (uiop:dump-image
"quicklisp-to-nix-system-info" :executable t
))