-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmanager.lisp
454 lines (351 loc) · 14.7 KB
/
manager.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
;;; manager.lisp
;;; This Source Code Form is subject to the terms of the Mozilla Public
;;; License, v. 2.0. If a copy of the MPL was not distributed with this
;;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(defpackage #:cl-manager
(:nicknames #:clm)
(:use :cl)
(:export
#:env
#:install
#:update-index
#:current-directory-search
#:dot-clm-directory-search
#:register-search-functions
#:load-system
#:update
#:find-system
#:add-local-system
#:load-systems
#:*system-blacklist*
#:write-boot-file
#:info))
(in-package #:cl-manager)
;;; user options
(defvar *env* (uiop:getcwd)
"The cl-manager working directory")
(defvar *system-blacklist* (list "asdf" "uiop")
"List of blacklisted systems.
Each member won't be installed when it is found between the
dependencies of a system. This possibly can break some stuff. So I
guess you know what you're doing.")
;;; variables
(defvar *index-url* "https://raw.githubusercontent.com/rudolfochrist/clm-projects/master/systems.txt")
(defvar *index-version-url*
"https://raw.githubusercontent.com/rudolfochrist/clm-projects/master/version.txt")
(defvar *local-index-file* (merge-pathnames "clm/local.txt" (uiop:xdg-data-home)))
(defvar *index* nil)
;;; conditions
(define-condition clm-error (error)
((message :accessor clm-errro-message
:initarg :message))
(:report (lambda (condition stream)
(format stream "~A" (clm-errro-message condition)))))
(define-condition no-env-error (clm-error)
())
(define-condition system-not-found (clm-error)
((system :accessor missing-system
:initarg :system))
(:report (lambda (condition stream)
(format stream "System ~A not found!" (missing-system condition)))))
(define-condition uninitialized-git (clm-error)
()
(:report (lambda (condition stream)
(declare (ignore condition))
(format stream "Not a git repository: ~A~%Please run `git init'." (env)))))
;;; helpers
(defstruct %system
project system-name source ref dependencies)
(defun clm-install-directory ()
(merge-pathnames ".clm/" (env)))
(defun empty-string-p (string)
(= 0 (length string)))
(defun exec (command &key verbose)
(let ((output (uiop:run-program command
:output '(:string :stripped t)
:error-output :output)))
(prog1
output
(when verbose
(qprint output)))))
(defun curl-file (url filename)
(exec (format nil "curl -fsSL ~A -o ~A" url filename)))
(defun curl (url)
(exec (format nil "curl -fsSL ~A" url)))
(defun git-ref (system &key verbose)
(exec `("git"
"--git-dir" ,(format nil ".clm/~A/.git" (%system-project system))
"rev-parse" "--verify" "HEAD")
:verbose verbose))
(defun qprint (message &optional stream &rest arguments)
(if (null arguments)
(format (or stream t) "~&; ~A~%" message)
(apply #'format
stream
(format nil "~&; ~A~%" message)
arguments)))
(defun hash-keys (hash-table)
(loop for k being the hash-keys of hash-table
collect k))
(defun hash-values (hash-table)
(loop for v being the hash-values of hash-table
collect v))
(defun alist-to-hash-table (alist)
(loop with hash-table = (make-hash-table :test 'equal)
for (key . value) in alist
do (setf (gethash key hash-table) value)
finally (return hash-table)))
(defun parse-clmfile (clmfile)
(with-open-file (f clmfile)
(loop with deps = (make-hash-table :test 'equal)
for line = (read-line f nil nil)
while line
when (and (not (empty-string-p line))
(not (uiop:string-prefix-p "#" line)))
do (destructuring-bind (system-name &optional ref)
(uiop:split-string line)
(setf (gethash system-name deps)
(make-%system :system-name system-name
:ref ref)))
finally (return deps))))
(defun make-index-table (systems-file &key merge-index)
(with-open-file (f systems-file)
(loop with index = (if merge-index
*index*
(make-hash-table :test 'equal))
for line = (read-line f nil nil)
while line
when (and (not (empty-string-p line))
(not (uiop:string-prefix-p "#" line)))
do (destructuring-bind (project system-name source &rest dependencies)
(uiop:split-string line)
(setf (gethash system-name index)
(make-%system :project project
:system-name system-name
:source source
:dependencies (remove-if #'empty-string-p dependencies))))
finally (setf *index* index)))
*index*)
(defun blacklistp (name)
(member name *system-blacklist* :test #'string=))
(defun resolve-dependencies (deps)
(loop with q = (hash-keys deps)
while q
do (let ((dep-name (pop q)))
(unless (blacklistp dep-name)
(multiple-value-bind (dep foundp)
(gethash dep-name deps)
(let ((%system (find-system dep-name)))
(if foundp
(setf (%system-project dep) (%system-project %system)
(%system-source dep) (%system-source %system))
(setf (gethash dep-name deps)
(make-%system :project (%system-project %system)
:system-name dep-name
:source (%system-source %system))))
(setf q (append q (%system-dependencies %system)))))))
finally (return (unique-project-dependencies deps))))
(defun unique-project-dependencies (dependency-table)
(loop for dep being the hash-values of dependency-table
collect dep into deps
finally (return (remove-duplicates deps :test #'string= :key #'%system-project))))
(defun write-lockfile (dependencies)
(with-open-file (out (merge-pathnames "clm.lock" (env))
:direction :output
:if-exists :supersede)
(loop for dep in dependencies
do (format out "~&~A ~A~@[ ~A~]~%"
(%system-project dep)
(%system-source dep)
(%system-ref dep)))))
(defun read-lockfile (lockfile)
(with-open-file (in lockfile)
(loop for line = (read-line in nil nil)
while line
collect (destructuring-bind (project source &optional ref)
(uiop:split-string line)
(make-%system :project project
:source source
:ref ref)))))
(defun download-dependencies (deps &key verbose)
(loop for dep in deps
do (qprint "Installing ~A" t (%system-project dep))
unless (probe-file (merge-pathnames (format nil ".clm/~A" (%system-project dep)) (env)))
do (exec
(format nil "git clone --depth 1~@[ -b ~A~] ~A .clm/~A"
(%system-ref dep)
(%system-source dep)
(%system-project dep))
:verbose verbose)
finally (return (values))))
(defun read-index-version ()
(let ((systems-file (asdf:system-relative-pathname "cl-manager" "systems.txt")))
(when (probe-file systems-file)
(with-open-file (stream systems-file)
(subseq (read-line stream) 2)))))
(defun get-remote-index-version ()
(curl *index-version-url*))
(defun add-refs (deps &key verbose)
(loop for system in deps
when (null (%system-ref system))
do (setf (%system-ref system)
(git-ref system :verbose verbose))))
;;; API
(defun env ()
"Path to the CLM environment."
(if (null *env*)
(restart-case
(error 'no-env-error
:message "Environment not initialized.")
(use-cwd ()
:report (lambda (stream)
(format stream "Set dep-name directory (~A) as environment."
(uiop:getcwd)))
(setf (env) (uiop:getcwd))))
*env*))
(defun (setf env) (path)
"Set the CLM environment to PATH."
(setf *env* path))
(defun find-system (name &optional (error t))
"Lookup system with NAME in index."
(multiple-value-bind (system foundp)
(gethash (etypecase name
(string name)
(symbol (string-downcase (string name))))
*index*)
(cond
(foundp system)
(error (error 'system-not-found :system name))
(t nil))))
(defun update-index (&optional (url *index-url*))
(let ((systems-file (asdf:system-relative-pathname "cl-manager" "systems.txt"))
(local-version (read-index-version))
(remote-version (get-remote-index-version)))
(if (string= local-version remote-version)
(qprint "Latest version ~A already installed." t remote-version)
(progn
(qprint "Updating index to version ~A." t remote-version)
(curl-file url systems-file)))
(make-index-table systems-file))
;; local index
(dolist (index-file (uiop:directory-files (uiop:pathname-directory-pathname *local-index-file*)))
(make-index-table index-file :merge-index t)))
(defun install (&key fresh verbose)
"Install systems defined in CLMFILE."
(when (or fresh
(not (probe-file (merge-pathnames "clm.lock" (env)))))
(write-lockfile (resolve-dependencies (parse-clmfile (merge-pathnames "clmfile" (env))))))
(when fresh
(uiop:delete-directory-tree (merge-pathnames ".clm/" (env)) :validate t))
(let ((deps (read-lockfile (merge-pathnames "clm.lock" (env)))))
(download-dependencies deps :verbose verbose)
(add-refs deps :verbose verbose)
(write-lockfile deps)))
(defun add-to-clmfile (name &optional ref)
(with-open-file (stream (merge-pathnames "clmfile" (env))
:direction :output
:if-exists :append
:if-does-not-exist :create)
(format stream "~&~A~@[ ~A~]~%" name ref)))
(defun update ()
"Updated dependencies."
(delete-file (merge-pathnames "clm.lock" (env)))
(install))
(defun grab-missing (name)
;; TODO: this is so ugly, but I don't know any other way to access
;; the condition inside the the restart function.
(handler-case
(asdf:load-system name)
(asdf/find-component:missing-dependency (condition)
(asdf/find-component:missing-requires condition))))
(defmethod load-system ((name string) &key verbose silent force load-tests)
"Load system with NAME.
If VERBOSE is non-nil display verbose output."
(let ((*load-verbose* nil)
(*compile-verbose* nil)
(*load-print* nil)
(*compile-print* nil))
(unless silent
(qprint "Loading ~A." *standard-output* name))
(handler-bind (#+sbcl (sb-ext:compiler-note #'muffle-warning)
(warning #'muffle-warning))
(restart-case
(asdf:load-system name :verbose verbose :force force)
(add-dependency ()
:test (lambda (c)
(and (typep c 'asdf/find-component:missing-dependency)
(clm:find-system (asdf/find-component:missing-requires c) nil)))
:report (lambda (stream)
(format stream "Add missing dependency to clmfile, run update and try again loading?"))
(let ((missing (grab-missing name)))
(add-to-clmfile missing)
(install)
(load-system name
:verbose verbose
:silent silent
:force force
:load-tests load-tests))))
(when load-tests
(load-system (concatenate 'string name "/test")
:verbose verbose
:silent silent
:force force))))
t)
(defmethod load-system ((name symbol) &key verbose silent force ref add load-tests)
(load-system (string-downcase (string name))
:verbose verbose
:silent silent
:force force
:ref ref
:add add
:load-tests load-tests))
(defun load-systems (systems &key verbose silent force)
"Load a list of systems at once.
VERBOSE, SILENT, FORCE a passed as is to LOAD-SYSTEM."
(mapc (lambda (system)
(load-system system :verbose verbose :silent silent :force force))
systems))
(defun current-directory-search (name)
"Search the current directory for system NAME."
(probe-file (make-pathname :defaults (uiop:getcwd)
:name (asdf:primary-system-name name)
:type "asd")))
(defun dot-clm-directory-search (name)
"Search .clm directory for system NAME."
(handler-case
(find-if (lambda (p)
(and (string= (asdf:primary-system-name name) (pathname-name p))
(string= "asd" (pathname-type p))))
(uiop:directory-files (merge-pathnames ".clm/" (env)) "**/*.asd"))
(no-env-error (condition)
(declare (ignore condition)))))
(defun register-search-functions ()
(dolist (sf (list 'current-directory-search 'dot-clm-directory-search))
(pushnew sf asdf:*system-definition-search-functions* :test #'eq))
(setf asdf:*system-definition-search-functions*
(nreverse asdf:*system-definition-search-functions*)))
(defun add-local-system (project system-name source &rest dependencies)
"Add a system specification to the local index.
This can also be used to \"update\" systems with local informaton
e.g. using a different fork."
(ensure-directories-exist *local-index-file*)
(with-open-file (out *local-index-file*
:direction :output
:if-exists :append
:if-does-not-exist :create)
(format out "~&~A ~A ~A~{ ~A~}~%"
project system-name source dependencies)))
(defun write-boot-file (&optional (path (env)))
(uiop:copy-file
(asdf:system-relative-pathname "cl-manager" "templates/boot.lisp")
(merge-pathnames "boot.lisp" path)))
(defun info ()
"Print clm-manager informaton."
(qprint "cl-manager:")
(qprint " - ENVIRONMENT: ~A" t (env))
(qprint " - CL-MANAGER VERSION: ~A" t (asdf:component-version (asdf:find-system "cl-manager")))
(qprint " - INDEX VERSION: ~A" t (read-index-version))
(qprint " - INSTALLATION DIRECTORY: ~A" t (asdf:system-source-directory "cl-manager"))
(qprint " - INDEX URL: ~A" t *index-url*)
(qprint " - INDEX VERSION URL: ~A" t *index-version-url*)
(qprint " - LOCAL INDEX FILE: ~A " t *local-index-file*))