forked from quicklisp/quicklisp-client
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dist.lisp
775 lines (631 loc) · 23.8 KB
/
dist.lisp
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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
;;;; dist.lisp
(in-package #:ql-dist)
;;; Generic functions
(defgeneric dist (object)
(:documentation
"Return the dist of OBJECT."))
(defgeneric release (object)
(:documentation
"Return the release of OBJECT."))
(defgeneric system (object)
(:documentation
"Return the system of OBJECT."))
(defgeneric name (object)
(:documentation
"Return the name of OBJECT."))
(defgeneric find-system (name)
(:documentation
"Return a system with the given NAME, or NIL if no system is
found. If multiple systems have the same name, the one with the
highest preference is returned."))
(defgeneric find-release (name)
(:documentation
"Return a release with the given NAME, or NIL if no system is
found. If multiple releases have the same name, the one with the
highest preference is returned."))
(defgeneric find-systems-named (name)
(:documentation
"Return a list of all systems in all enabled dists with the given
NAME, sorted by preference."))
(defgeneric find-releases-named (name)
(:documentation
"Return a list of all releases in all enabled dists with the given
NAME, sorted by preference."))
(defgeneric base-directory (object)
(:documentation
"Return the base directory pathname of OBJECT."))
(defgeneric relative-to (object pathname)
(:documentation
"Merge PATHNAME with the base-directory of OBJECT.")
(:method (object pathname)
(merge-pathnames pathname (base-directory object))))
(defgeneric enabledp (object)
(:documentation
"Return true if OBJECT is enabled."))
(defgeneric enable (object)
(:documentation
"Enable OBJECT."))
(defgeneric disable (object)
(:documentation
"Disable OBJECT."))
(defgeneric installedp (object)
(:documentation
"Return true if OBJECT is installed."))
(defgeneric install (object)
(:documentation
"Install OBJECT."))
(defgeneric ensure-installed (object)
(:documentation
"Ensure that OBJECT is installed.")
(:method (object)
(unless (installedp object)
(install object))
object))
(defgeneric uninstall (object)
(:documentation
"Uninstall OBJECT."))
(defgeneric metadata-name (object)
(:documentation
"The metadata-name of an object is used to form the pathname for a
few different object metadata files."))
(defgeneric install-metadata-file (object)
(:documentation
"The pathname to a file describing the installation status of
OBJECT."))
(defgeneric preference-parent (object)
(:documentation
"Return a value suitable for checking if OBJECT has no specific
preference set.")
(:method (object)
nil))
(defgeneric preference-file (object)
(:documentation
"Return the file from which preference information is loaded for
OBJECT.")
(:method (object)
(relative-to object "preference.txt")))
(defgeneric preference (object)
(:documentation
"Returns a value used when comparing multiple systems or releases
with the same name. Objects with higher preference are returned by
FIND-SYSTEM and FIND-RELEASE.")
(:method ((object null))
0)
(:method (object)
(with-open-file (stream (preference-file object)
:if-does-not-exist nil)
(if stream
(values (parse-integer (read-line stream)))
(preference (preference-parent object))))))
(defgeneric (setf preference) (preference object)
(:documentation
"Set the preference for OBJECT. Objects with higher preference are
returned by FIND-SYSTEM and FIND-RELEASE.")
(:method (preference object)
(check-type preference integer)
(let ((preference-file (preference-file object)))
(ensure-directories-exist preference-file)
(with-open-file (stream (preference-file object)
:direction :output
:if-exists :supersede)
(format stream "~D" preference)))
preference))
(defgeneric forget-preference (object)
(:documentation
"Remove specific preference information for OBJECT.")
(:method (object)
(delete-file-if-exists (preference-file object))))
(defgeneric short-description (object)
(:documentation "Return a short string describing OBJECT."))
(defgeneric provided-releases (object)
(:documentation "Return a list of releases provided by OBJECT."))
(defgeneric provided-systems (object)
(:documentation "Return a list of systems provided by OBJECT."))
(defgeneric installed-releases (dist)
(:documentation
"Return a list of all releases installed for DIST.")
(:method (dist)
(remove-if-not #'installedp (provided-releases dist))))
(defgeneric installed-systems (dist)
(:documentation
"Return a list of all systems installed for DIST.")
(:method (dist)
(remove-if-not #'installedp (provided-systems dist))))
(defgeneric new-version-available-p (dist)
(:documentation
"Return true if a new version of DIST is available."))
(defgeneric find-system-in-dist (system-name dist)
(:documentation
"Return a system with the given NAME in DIST, or NIL if no system
is found."))
(defgeneric find-release-in-dist (release-name dist)
(:documentation
"Return a release with the given NAME in DIST, or NIL if no release
is found."))
(defgeneric ensure-system-index-file (dist)
(:documentation
"Return the pathname for the system index file of DIST, fetching it
from a remote source first if necessary."))
(defgeneric ensure-release-index-file (dist)
(:documentation
"Return the pathname for the release index file of DIST, fetching
it from a remote source first if necessary."))
(defgeneric initialize-release-index (dist)
(:documentation
"Initialize the release index of DIST."))
(defgeneric initialize-system-index (dist)
(:documentation
"Initialize the system index of DIST."))
(defgeneric local-archive-file (release)
(:documentation
"Return the pathname to where the archive file of RELEASE should be
stored."))
(defgeneric ensure-local-archive-file (release)
(:documentation
"If the archive file for RELEASE is not available locally, fetch it
and return the pathname to it."))
(defgeneric local-archive-file-valid-p (release)
(:documentation
"Check the local archive file of RELEASE for validity, including
size and signature checks."))
(defgeneric archive-url (release)
(:documentation
"Return the full URL for fetching the archive file of RELEASE."))
(defgeneric installed-asdf-system-file (object)
(:documentation
"Return the path to the installed ASDF system file for OBJECT, or
NIL if there is no installed system file."))
(eval-when (:compile-toplevel :load-toplevel :execute)
(defmacro destructure-line (lambda-list line &body body)
`(destructuring-bind ,lambda-list
(split-spaces ,line)
,@body))
(defun call-for-each-line (fun file)
(with-open-file (stream file)
(loop for line = (read-line stream nil)
while line do (funcall fun line))))
(defmacro for-each-line ((line file) &body body)
`(call-for-each-line (lambda (,line) ,@body) ,file)))
(defun make-line-instance (line class &rest initargs)
"Create an instance from words in an index file line. The last initarg collects all the trailing arguments, if any."
(let* ((words (split-spaces line))
(args (mapcan #'list
(butlast initargs)
words))
(trailing (subseq words (1- (length initargs)))))
(apply #'make-instance class (first (last initargs)) trailing args)))
(defun ignorable-line (line)
(labels ((blank-char-p (char)
(member char '(#\Space #\Tab)))
(blankp (line)
(every #'blank-char-p line))
(ignorable (line)
(or (zerop (length line))
(blankp line)
(eql (char line 0) #\#))))
(ignorable line)))
(defun symbol-case (string)
(if (upper-case-p (char (string '#:foo) 0))
(string-upcase string)
string))
(defun config-file-initargs (file)
(let ((initargs '()))
(for-each-line (line file)
(unless (ignorable-line line)
(destructure-line (initarg value)
line
(let ((keyword (intern (symbol-case (string-right-trim '(#\:)
initarg))
:keyword)))
(push value initargs)
(push keyword initargs)))))
initargs))
;;;
;;; A few generic things
;;;
(defmethod dist ((name symbol))
(dist (string name)))
(defmethod dist ((name string))
(find-dist (string-downcase name)))
(defmethod release ((name symbol))
(release (string name)))
(defmethod release ((name string))
(find-release (string-downcase name)))
(defmethod system ((name symbol))
(system (string name)))
(defmethod system ((name string))
(find-system (string-downcase name)))
;;;
;;; Dists
;;;
;;; A dist is a set of releases.
;;;
(defclass dist ()
((base-directory
:initarg :base-directory
:accessor base-directory)
(name
:initarg :name
:accessor name)
(version
:initarg :version
:accessor version)
(system-index-url
:initarg :system-index-url
:accessor system-index-url)
(release-index-url
:initarg :release-index-url
:accessor release-index-url)
(archive-base-url
:initarg :archive-base-url
:accessor archive-base-url)
(canonical-distinfo-url
:initarg :canonical-distinfo-url
:accessor canonical-distinfo-url)
(distinfo-subscription-url
:initarg :distinfo-subscription-url
:accessor distinfo-subscription-url)
(system-index
:initarg :system-index
:accessor system-index)
(release-index
:initarg :release-index
:accessor release-index)
(local-distinfo-file
:initarg :local-distinfo-file
:accessor local-distinfo-file))
(:default-initargs
:name "unnamed"
:version "unknown"))
(defmethod short-description ((dist dist))
(format nil "~A ~A" (name dist) (version dist)))
(defmethod print-object ((dist dist) stream)
(print-unreadable-object (dist stream :type t)
(write-string (short-description dist) stream)))
(defmethod provided-releases ((dist dist))
(loop for release being each hash-value of (release-index dist)
collect release))
(defmethod provided-systems ((dist dist))
(loop for system being each hash-value of (system-index dist)
collect system))
(defmethod ensure-release-index-file ((dist dist))
(let ((pathname (relative-to dist "releases.txt")))
(or (probe-file pathname)
(nth-value 1 (fetch (release-index-url dist) pathname)))))
(defmethod ensure-system-index-file ((dist dist))
(let ((pathname (relative-to dist "systems.txt")))
(or (probe-file pathname)
(nth-value 1 (fetch (system-index-url dist) pathname)))))
(defun dist-name-pathname (name)
"Return the pathname that would be used for an installed dist with
the given NAME."
(qmerge (make-pathname :directory (list :relative "dists" name))))
(defmethod slot-unbound (class (dist dist) (slot (eql 'base-directory)))
(setf (base-directory dist) (dist-name-pathname (name dist))))
(defun make-dist-from-file (file)
"Load dist info from FILE and use it to create a dist instance."
(let ((initargs (config-file-initargs file)))
(apply #'make-instance 'dist :local-distinfo-file file initargs)))
(defmethod install-metadata-file ((dist dist))
(relative-to dist "distinfo.txt"))
(defun find-dist (name)
(let ((pathname (merge-pathnames "distinfo.txt"
(dist-name-pathname name))))
(when (probe-file pathname)
(make-dist-from-file pathname))))
(defmethod enabledp ((dist dist))
(not (not (probe-file (relative-to dist "enabled.txt")))))
(defmethod enable ((dist dist))
(ensure-file-exists (relative-to dist "enabled.txt"))
t)
(defmethod disable ((dist dist))
(delete-file-if-exists (relative-to dist "enabled.txt"))
t)
(defmethod installedp ((dist dist))
(let ((installed (find-dist (name dist))))
(equalp (version installed) (version dist))))
(defmethod find-release-in-dist (release dist)
(values (gethash release (release-index dist))))
(defparameter *dist-enumeration-functions*
'(standard-dist-enumeration-function)
"ALL-DISTS calls each function in this list with no arguments, and
appends the results into a list of dist objects, removing
duplicates. Functions might be called just once for a batch of
related operations; see WITH-CONSISTENT-DISTS.")
(defun standard-dist-enumeration-function ()
"The default function used for producing a list of dist objects."
(loop for file in (directory (qmerge "dists/*/distinfo.txt"))
collect (make-dist-from-file file)))
(defun all-dists ()
"Return a list of all known dists."
(remove-duplicates
(apply 'append (mapcar 'funcall *dist-enumeration-functions*))))
(defun enabled-dists ()
"Return a list of all known dists for which ENABLEDP returns true."
(remove-if-not #'enabledp (all-dists)))
(defmethod install-metadata-file (object)
(relative-to (dist object)
(make-pathname :directory
(list :relative "installed"
(metadata-name object))
:name (name object)
:type "txt")))
(defclass preference-mixin () ()
(:documentation
"Instances of this class have a special location for their
preference files."))
(defmethod preference-file ((object preference-mixin))
(relative-to
(dist object)
(make-pathname :directory (list :relative
"preferences"
(metadata-name object))
:name (name object)
:type "txt")))
;;;
;;; Releases
;;;
(defclass release (preference-mixin)
((project-name
:initarg :project-name
:accessor name
:accessor project-name)
(dist
:initarg :dist
:accessor dist
:reader preference-parent)
(provided-systems
:initarg :provided-systems
:accessor provided-systems)
(archive-url
:initarg :archive-url
:accessor archive-url)
(archive-size
:initarg :archive-size
:accessor archive-size)
(archive-md5
:initarg :archive-md5
:accessor archive-md5)
(archive-content-sha1
:initarg :archive-content-sha1
:accessor archive-content-sha1)
(prefix
:initarg :prefix
:accessor prefix
:reader short-description)
(system-files
:initarg :system-files
:accessor system-files)
(metadata-name
:initarg :metadata-name
:accessor metadata-name))
(:default-initargs
:metadata-name "releases")
(:documentation
"Instances of this class represent a snapshot of a project at some
point in time, which might be from version control, or from an
official release, or from some other source."))
(defmethod print-object ((release release) stream)
(print-unreadable-object (release stream :type t)
(format stream "~A / ~A"
(short-description release)
(short-description (dist release)))))
(defmethod local-archive-file-valid-p ((release release))
t)
(defmethod local-archive-file ((release release))
(relative-to (dist release)
(make-pathname :directory '(:relative "archives")
:defaults (file-namestring
(path (url (archive-url release)))))))
(defmethod ensure-local-archive-file ((release release))
(let ((pathname (local-archive-file release)))
(or (probe-file pathname)
(progn
(ensure-directories-exist pathname)
(nth-value 1 (fetch (archive-url release) pathname))))))
(defmethod base-directory ((release release))
(relative-to
(dist release)
(make-pathname :directory (list :relative "software" (prefix release)))))
(defmethod installedp ((release release))
(and (probe-file (install-metadata-file release))
(every #'installedp (provided-systems release))))
(defmethod install ((release release))
(let ((archive (ensure-local-archive-file release))
(tar (qmerge "tmp/release-install.tar"))
(output (relative-to (dist release)
(make-pathname :directory
(list :relative "software"))))
(tracking (install-metadata-file release)))
(ensure-directories-exist tar)
(ensure-directories-exist output)
(ensure-directories-exist tracking)
(gunzip archive tar)
(unpack-tarball tar :directory output)
(ensure-directories-exist tracking)
(with-open-file (stream tracking
:direction :output
:if-exists :supersede)
(write-line (qenough (base-directory release)) stream))
(let ((provided (provided-systems release))
(dist (dist release)))
(dolist (file (system-files release))
(let ((system (find-system-in-dist (pathname-name file) dist)))
(unless (member system provided)
(error "FIND-SYSTEM-IN-DIST returned ~A but I expected one of ~A"
system provided))
(let ((system-tracking (install-metadata-file system))
(system-file (merge-pathnames file
(base-directory release))))
(ensure-directories-exist system-tracking)
(unless (probe-file system-file)
(error "Release claims to have ~A, but I can't find it"
system-file))
(with-open-file (stream system-tracking
:direction :output
:if-exists :supersede)
(write-line (qenough system-file)
stream))))))
release))
(defun call-for-each-index-entry (file fun)
(labels ((blank-char-p (char)
(member char '(#\Space #\Tab)))
(blankp (line)
(every #'blank-char-p line))
(ignorable (line)
(or (zerop (length line))
(blankp line)
(eql (char line 0) #\#))))
(with-open-file (stream file)
(loop for line = (read-line stream nil)
while line do
(unless (ignorable line)
(funcall fun line))))))
(defmethod initialize-release-index ((dist dist))
(let ((releases (ensure-release-index-file dist))
(index (make-hash-table :test 'equal)))
(call-for-each-index-entry
releases
(lambda (line)
(let ((instance (make-line-instance line 'release
:project-name
:archive-url
:archive-size
:archive-md5
:archive-content-sha1
:prefix
:system-files)))
(setf (dist instance) dist)
(setf (archive-size instance) (parse-integer (archive-size instance)))
(setf (gethash (project-name instance) index) instance))))
(setf (release-index dist) index)))
(defmethod slot-unbound (class (dist dist) (slot (eql 'release-index)))
(initialize-release-index dist))
;;;
;;; Systems
;;;
;;; A "system" in the defsystem sense.
;;;
(defclass system (preference-mixin)
((name
:initarg :name
:accessor name
:reader short-description)
(system-file-name
:initarg :system-file-name
:accessor system-file-name)
(release
:initarg :release
:accessor release
:reader preference-parent)
(dist
:initarg :dist
:accessor dist)
(required-systems
:initarg :required-systems
:accessor required-systems)
(metadata-name
:initarg :metadata-name
:accessor metadata-name))
(:default-initargs
:metadata-name "systems"))
(defmethod print-object ((system system) stream)
(print-unreadable-object (system stream :type t)
(format stream "~A / ~A / ~A"
(short-description system)
(short-description (release system))
(short-description (dist system)))))
(defmethod provided-systems ((system system))
(list system))
(defmethod initialize-system-index ((dist dist))
(let ((systems (ensure-system-index-file dist))
(index (make-hash-table :test 'equal)))
(call-for-each-index-entry
systems
(lambda (line)
(let ((instance (make-line-instance line 'system
:release
:system-file-name
:name
:required-systems)))
(let ((release (find-release-in-dist (release instance) dist)))
(setf (release instance) release)
(if (slot-boundp release 'provided-systems)
(pushnew instance (provided-systems release))
(setf (provided-systems release) (list instance))))
(setf (dist instance) dist)
(setf (gethash (name instance) index) instance))))
(setf (system-index dist) index)))
(defmethod slot-unbound (class (release release) (slot (eql 'provided-systems)))
(initialize-system-index (dist release))
(if (slot-boundp release 'provided-systems)
(provided-systems release)
(setf (provided-systems release) nil)))
(defmethod slot-unbound (class (dist dist) (slot (eql 'system-index)))
(initialize-system-index dist))
(defmethod find-system-in-dist (system-name dist)
(values (gethash system-name (system-index dist))))
(defmethod preference ((system system))
(if (probe-file (preference-file system))
(call-next-method)
(preference (release system))))
(defun find-thing-named (find-fun name)
(let ((result '()))
(dolist (dist (enabled-dists) (sort result #'> :key #'preference))
(let ((thing (funcall find-fun name dist)))
(when thing
(push thing result))))))
(defmethod find-systems-named (name)
(find-thing-named #'find-system-in-dist name))
(defmethod find-releases-named (name)
(find-thing-named #'find-release-in-dist name))
(defmethod find-system (name)
(first (find-systems-named name)))
(defmethod find-release (name)
(first (find-releases-named name)))
(defmethod install ((system system))
(ensure-installed (release system)))
(defmethod install-metadata-file ((system system))
(relative-to (dist system)
(make-pathname :name (system-file-name system)
:type "txt"
:directory '(:relative "installed" "systems"))))
(defmethod installed-asdf-system-file ((system system))
(let ((metadata-file (install-metadata-file system)))
(when (probe-file metadata-file)
(with-open-file (stream metadata-file)
(let* ((relative (read-line stream))
(full (qmerge relative)))
(when (probe-file full)
full))))))
(defmethod installedp ((system system))
(installed-asdf-system-file system))
(defun find-asdf-system-file (name)
(let ((system (find-system name)))
(when system
(installed-asdf-system-file system))))
(defun call-with-consistent-dists (fun)
"Take a snapshot of the available dists and return the same list
consistently each time ALL-DISTS is called in the dynamic scope of
FUN."
(let* ((all-dists (all-dists))
(*dist-enumeration-functions* (list (constantly all-dists))))
(funcall fun)))
(defmacro with-consistent-dists (&body body)
"See CALL-WITH-CONSISTENT-DISTS."
`(call-with-consistent-dists (lambda () ,@body)))
(defgeneric dependency-tree (system)
(:method ((string string))
(dependency-tree (find-system string)))
(:method ((system system))
(with-consistent-dists
(list* system (mapcar 'dependency-tree (required-systems system))))))
(defmethod provided-systems ((object (eql t)))
(let ((systems (loop for dist in (all-dists)
appending (provided-systems dist))))
(sort systems #'string< :key #'name)))
(defgeneric system-apropos (term)
(:method ((term string))
(dolist (system (provided-systems t))
(when (search term (name system))
(format t "~A~%" system)))))