New Script -- run-parts
[pkgusr] / lisp / pkgusr.el
1 ;; pkgusr.el --- elisp tools for LFS pkgusr package management   -*- Emacs-Lisp -*-
2
3 ;; Copyright (C) 2007 - 2014 Steve Youngs
4
5 ;; Author:     Steve Youngs <steve@sxemacs.org>
6 ;; Maintainer: Steve Youngs <steve@sxemacs.org>
7 ;; Created:    <2007-07-13>
8 ;; Time-stamp: <Friday Feb 12, 2021 16:19:09 steve>
9 ;; Homepage:   N/A
10 ;; Keywords:   utils package-management
11
12 ;; This file is part of pkgusr.
13
14 ;; Redistribution and use in source and binary forms, with or without
15 ;; modification, are permitted provided that the following conditions
16 ;; are met:
17 ;;
18 ;; 1. Redistributions of source code must retain the above copyright
19 ;;    notice, this list of conditions and the following disclaimer.
20 ;;
21 ;; 2. Redistributions in binary form must reproduce the above copyright
22 ;;    notice, this list of conditions and the following disclaimer in the
23 ;;    documentation and/or other materials provided with the distribution.
24 ;;
25 ;; 3. Neither the name of the author nor the names of any contributors
26 ;;    may be used to endorse or promote products derived from this
27 ;;    software without specific prior written permission.
28 ;;
29 ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
30 ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
31 ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
32 ;; DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
33 ;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
34 ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
35 ;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
36 ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
37 ;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
38 ;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
39 ;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
41 ;;; Commentary:
42 ;; 
43 ;;   This is a collection of tools I use with package management here
44 ;;   on bastard.  A lot of them are elisp ports of some shell functions
45 ;;   I use for the same.
46
47 ;;; Todo:
48 ;;
49 ;;   o Reduce code duplication through use of macros
50 ;;   o View build logs
51 ;;   o Run builds
52 ;;
53
54 ;;; Code:
55 (defvar pkgusr-pkg-history nil
56   "History for pkgusr.")
57
58 ;; Errors
59 (define-error 'pkgusr-unknown-cmd "Can't find command")
60 (define-error 'pkgusr-unknown-file "Don't recognise file")
61 (define-error 'pkgusr-unknown-pkg "Unknown package")
62
63 (defun pkgusr-all-pkgs ()
64   "Return a list of all installed packages."
65   (let ((pkgs ""))
66     (with-temp-buffer
67       (insert-file-contents "/etc/group")
68       (catch 'done
69         (while t
70           (if (re-search-forward "^install:x:9999:" nil t)
71               (setq pkgs (concat pkgs
72                                  (buffer-substring (point)
73                                                    (point-at-eol))
74                                  ","))
75             (throw 'done nil))))
76       (split-string-by-char (substring pkgs 0 -1) ?,))))
77
78 (defvar pkgusr-all-pkgs (pkgusr-all-pkgs)
79   "A list of all installed packages.")
80
81 (defun pkgusr-all-pkgs-update ()
82   "Update the list of installed packages."
83   (setq pkgusr-all-pkgs (pkgusr-all-pkgs)))
84
85 (defun pkgusr-pkgs-count (&optional upd-list)
86   "Return the number of installed packages.
87
88 With optional argument UPD-LIST force an update of the packages list."
89   (and upd-list
90        (pkgusr-all-pkgs-update))
91   (length pkgusr-all-pkgs))
92
93 (defun pkgusr-list-pkgs-regexp (regexp &optional upd-list)
94   "Return a list of packages matching REGEXP.
95
96 With 1 prefix arg  insert the result into the current buffer at point.
97 With 2 prefix args force update of the packages list.
98 With 3 prefix args force update and insert into buffer.
99
100 With optional argument UPD-LIST, force update of the packages list."
101   (interactive "sRegexp: \np")
102   (and (or (eq upd-list 16)
103            (eq upd-list 64))
104        (pkgusr-all-pkgs-update))
105   (let ((case-fold-search t)
106         res)
107     (mapcar
108      #'(lambda (pkg)
109          (when (string-match regexp pkg)
110            (setq res (append res (list pkg)))))
111      pkgusr-all-pkgs)
112     (if (interactive-p)
113         (cond
114          ((or (eq upd-list 4)
115               (eq upd-list 64))
116           (insert (mapconcat #'identity res " ")))
117          (t
118           (message "[Matches for \"%s\"]: %s" regexp
119                    (mapconcat #'identity res " "))))
120       res)))
121
122 (defconst pkgusr-url-regexp
123   (concat
124    #r"\(\(https?\|ftp\|rsync\|s\(cp\|sh\)\|git\)://\|file:/\|s?news:\|mailto:\)"
125    "[^]\t\n \"'()<>[^`{}]*[^]\t\n \"'()<>[^`{}.,;]+")
126   "A regular expression matching URLs.")
127
128 (defun pkgusr-url-at-point ()
129   "Browse to a URL from the `pkgusr-show-pkg' buffer."
130   (interactive)
131   (when (extentp (extent-at (point)))
132     (browse-url (extent-string (extent-at (point))))))
133
134 (defun pkgusr-url-at-mouse (event)
135   "Browse to a URL at EVENT via the mouse from the `pkgusr-show-pkg' buffer."
136   (interactive "e")
137   (when (extentp (extent-at-event event))
138     (browse-url (extent-string (extent-at-event event)))))
139
140 (defconst pkgusr-ext-map
141   (let* ((map (make-sparse-keymap 'pkgusr-ext-map)))
142     (define-key map [button2] 'pkgusr-url-at-mouse)
143     (define-key map [return] 'pkgusr-url-at-point)
144     map)
145   "A keymap for the extents in the `pkgusr-show-pkg' buffer.")
146
147 (defun pkgusr-make-url-extents ()
148   "Create extent objects for all the URLs in the buffer."
149   (goto-char (point-min))
150   (save-excursion
151     (while (re-search-forward pkgusr-url-regexp nil t)
152       (let ((extent (make-extent (match-beginning 0) (match-end 0)))
153             (echo "RET or Button2 to visit this URL."))
154         (set-extent-property extent 'face 'bold)
155         (set-extent-property extent 'mouse-face 'highlight)
156         (set-extent-property extent 'keymap pkgusr-ext-map)
157         (set-extent-property extent 'help-echo echo)
158         (set-extent-property extent 'balloon-help echo)
159         (set-extent-property extent 'duplicable t)))))
160
161 (defun pkgusr-pkg-details (pkg)
162   (let ((buf (get-buffer-create (format "*Package Details: %s*" pkg)))
163         (detail (shell-command-to-string (format "pinky -l %s" pkg))))
164     (with-current-buffer buf
165       (erase-buffer)
166       (insert detail)
167       (pkgusr-make-url-extents))
168     (push-window-configuration)
169     (pop-to-buffer buf)
170     (view-mode nil
171                #'(lambda (b)
172                    (kill-buffer b)
173                    (pop-window-configuration)))))
174
175 (defun pkgusr-show-pkg ()
176   "Display filelist and other details for a package.
177
178 With a prefix arg, force update of the packages list."
179   (interactive)
180   (and current-prefix-arg
181        (pkgusr-all-pkgs-update))
182   (let ((pkg (completing-read "Show Package: "
183                               (mapcar #'list pkgusr-all-pkgs)
184                               nil t nil pkgusr-pkg-history)))
185     (pkgusr-pkg-details pkg)))
186
187 (defun pkgusr-pkg-install-notes ()
188   "Display the install notes of a package.
189
190 With a prefix arg, force update of the packages list."
191   (interactive)
192   (and current-prefix-arg
193        (pkgusr-all-pkgs-update))
194   (let ((pkg (completing-read "Package: "
195                               (mapcar #'list pkgusr-all-pkgs)
196                               nil t nil pkgusr-pkg-history))
197         start end)
198     (pkgusr-pkg-details pkg)
199     (re-search-forward "^Install Notes:$" nil t)
200     (setq start (point-at-bol))
201     (re-search-forward "^General Notes:$" nil t)
202     (setq end (point-at-bol))
203     (narrow-to-region start end)
204     (goto-char (point-min))))
205
206 (defun pkgusr-pkg-general-notes ()
207   "Display the general notes of a package.
208
209 With a prefix arg, force update of the packages list."
210   (interactive)
211   (and current-prefix-arg
212        (pkgusr-all-pkgs-update))
213   (let ((pkg (completing-read "Package: "
214                               (mapcar #'list pkgusr-all-pkgs)
215                               nil t nil pkgusr-pkg-history))
216         start end)
217     (pkgusr-pkg-details pkg)
218     (re-search-forward "^General Notes:$" nil t)
219     (setq start (point-at-bol))
220     (re-search-forward "^CONTENTS:$" nil t)
221     (setq end (point-at-bol))
222     (narrow-to-region start end)
223     (goto-char (point-min))))
224
225 (defun pkgusr-cmd-pkg (cmd)
226   "Display the package name \(user:group\) which contains CMD.
227
228 With a prefix arg, insert into the current buffer at point.
229
230 If non-interactive, return a list whose car is user and cdr is group."
231   (interactive "sCommand: ")
232   (let* ((cmd (or (executable-find cmd)
233                   (error 'pkgusr-unknown-cmd cmd)))
234          (user (user-login-name
235                 (nth 2 (file-attributes cmd))))
236          (group (or (declare-fboundp (user-group-name
237                                       (nth 3 (file-attributes cmd))))
238                     (user-login-name
239                      (nth 3 (file-attributes cmd))))))
240     (unless (or (member user pkgusr-all-pkgs)
241                 (member group pkgusr-all-pkgs))
242       (error 'pkgusr-unknown-pkg user))
243     (if (interactive-p)
244         (if current-prefix-arg
245             (insert (format "%s: (%s:%s)" cmd user group))
246           (message "Command: %s is from the \"%s\" package (%2$s:%s)"
247                    cmd user group))
248       (list user group))))
249
250 (defun pkgusr-file-pkg (file)
251   "Display the pkg name \(user:group\) which contains FILE.
252
253 With a prefix arg, insert into current buffer at point.
254
255 If non-interactive, return a list whose car is user and cdr is group."
256   (interactive "fFile: ")
257   (let ((user (user-login-name
258                (nth 2 (file-attributes file))))
259         (group (or (declare-fboundp (user-group-name
260                                      (nth 3 (file-attributes file))))
261                    (user-login-name
262                     (nth 3 (file-attributes file))))))
263     (unless (or (member user pkgusr-all-pkgs)
264                 (member group pkgusr-all-pkgs))
265       (error 'pkgusr-unknown-pkg user))
266     (if (interactive-p)
267         (if current-prefix-arg
268             (insert (format "%s: (%s:%s)" file user group))
269           (message "File: %s is from the \"%s\" package (%2$s:%s)"
270                    file user group)
271           (list user group)))))
272
273 (defun pkgusr-project-file (pkg)
274   "Return the .project file for PKG."
275   (let ((dir (paths-construct-path (list "/usr" "src" pkg))))
276     (expand-file-name ".project" dir)))
277
278 (defun pkgusr-pkg-url (&optional pkg upd-list)
279   "Return the URL of PKG as a string.
280
281 With 1 prefix arg, insert into current buffer at point.
282 With 2 prefix args, force update of the packages list.
283 With 3 prefix args, force update and insert into buffer.
284 With optional arg UPD-LIST, force update of the packages list."
285   (interactive)
286   (and (or (eq (car current-prefix-arg) 16)
287            (eq (car current-prefix-arg) 64)
288            upd-list)
289        (pkgusr-all-pkgs-update))
290   (let ((pkg (or pkg
291                  (completing-read "Package: "
292                                   (mapcar #'list pkgusr-all-pkgs)
293                                   nil t nil pkgusr-pkg-history)))
294         url)
295     (unless (member pkg pkgusr-all-pkgs)
296       (error 'pkgusr-unknown-pkg pkg))
297     (with-temp-buffer
298       (insert-file-contents (pkgusr-project-file pkg))
299       (goto-char (point-min))
300       (re-search-forward "Web_Site: <\\(.*\\)>$" nil t)
301       (setq url (match-string 1)))
302     (if (interactive-p)
303         (cond
304          ((or (eq (car current-prefix-arg) 4)
305               (eq (car current-prefix-arg) 64))
306           (insert url))
307          (t
308           (message "[URL (%s)]: %s" pkg url)))
309       url)))
310
311 (defun pkgusr-pkg-repo (&optional pkg upd-list)
312   "Return the repo URI of PKG as a string.
313
314 With 1 prefix arg, insert into current buffer at point.
315 With 2 prefix args, force update of the packages list.
316 With 3 prefix args, force update and insert into buffer.
317 With optional arg UPD-LIST, force update of the packages list."
318   (interactive)
319   (and (or (eq (car current-prefix-arg) 16)
320            (eq (car current-prefix-arg) 64)
321            upd-list)
322        (pkgusr-all-pkgs-update))
323   (let ((pkg (or pkg (completing-read "Package: "
324                                       (mapcar #'list pkgusr-all-pkgs)
325                                       nil t nil pkgusr-pkg-history)))
326         repo type)
327     (unless (member pkg pkgusr-all-pkgs)
328       (error 'pkgusr-unknown-pkg pkg))
329     (with-temp-buffer
330       (insert-file-contents (pkgusr-project-file pkg))
331       (goto-char (point-min))
332       (re-search-forward "Repo_Type: <\\(.*\\)>$" nil t)
333       (setq type (match-string 1))
334       (re-search-forward "Repo_Location: <\\(.*\\)>" nil t)
335       (setq repo (match-string 1)))
336     (if (interactive-p)
337         (cond
338          ((or (eq (car current-prefix-arg) 4)
339               (eq (car current-prefix-arg) 64))
340           (insert repo))
341          (t
342           (message "[Repo (%s)]: %s (%s)" pkg repo type)))
343       repo)))
344
345 (defun pkgusr-pkg-version (&optional pkg upd-list)
346   "Return the version of PKG as a string.
347
348 With 1 prefix arg, insert into current buffer at point.
349 With 2 prefix args, force update of the packages list.
350 With 3 prefix args, force update and insert into buffer.
351 With optional arg UPD-LIST, force update of the packages list."
352   (interactive)
353   (and (or (eq (car current-prefix-arg) 16)
354            (eq (car current-prefix-arg) 64)
355            upd-list)
356        (pkgusr-all-pkgs-update))
357   (let ((pkg (or pkg (completing-read "Package: "
358                                       (mapcar #'list pkgusr-all-pkgs)
359                                       nil t nil pkgusr-pkg-history)))
360         version)
361     (unless (member pkg pkgusr-all-pkgs)
362       (error 'pkgusr-unknown-pkg pkg))
363     (with-temp-buffer
364       (insert-file-contents (pkgusr-project-file pkg))
365       (goto-char (point-min))
366       (re-search-forward "Version: \\(.*$\\)" nil t)
367       (setq version (match-string 1)))
368     (if (interactive-p)
369         (cond
370          ((or (eq (car current-prefix-arg) 4)
371               (eq (car current-prefix-arg) 64))
372           (insert version))
373          (t
374           (message "[Version (%s)]: %s" pkg version)))
375       version)))
376
377 (defun pkgusr-pkg-description (&optional pkg upd-list)
378   "Return the description of PKG as a string.
379
380 With 1 prefix arg, insert into current buffer at point.
381 With 2 prefix args, force update of the packages list.
382 With 3 prefix args, force update and insert into buffer.
383 With optional arg UPD-LIST, force update of the packages list."
384   (interactive)
385   (and (or (eq (car current-prefix-arg) 16)
386            (eq (car current-prefix-arg) 64)
387            upd-list)
388        (pkgusr-all-pkgs-update))
389   (let ((pkg (or pkg (completing-read "Package: "
390                                       (mapcar #'list pkgusr-all-pkgs)
391                                       nil t nil pkgusr-pkg-history)))
392         desc)
393     (unless (member pkg pkgusr-all-pkgs)
394       (error 'pkgusr-unknown-pkg pkg))
395     (with-temp-buffer
396       (insert-file-contents (pkgusr-project-file pkg))
397       (goto-char (point-min))
398       (re-search-forward "Description: \\(.*$\\)" nil t)
399       (setq desc (match-string 1)))
400     (if (interactive-p)
401         (cond
402          ((or (eq (car current-prefix-arg) 4)
403               (eq (car current-prefix-arg) 64))
404           (insert desc))
405          (t
406           (message "[Description (%s)]: %s" pkg desc)))
407       desc)))
408
409 (defun pkgusr-pkg-deps (&optional pkg upd-list)
410   "Return a list of dependencies of PKG.
411
412 With 1 prefix arg, insert into current buffer at point.
413 With 2 prefix args, force update of the packages list.
414 With 3 prefix args, force update and insert into buffer.
415 With optional arg UPD-LIST, force update of the packages list."
416   (interactive)
417   (and (or (eq (car current-prefix-arg) 16)
418            (eq (car current-prefix-arg) 64)
419            upd-list)
420        (pkgusr-all-pkgs-update))
421   (let ((pkg (or pkg (completing-read "Package: "
422                                       (mapcar #'list pkgusr-all-pkgs)
423                                       nil t nil pkgusr-pkg-history)))
424         deps)
425     (unless (member pkg pkgusr-all-pkgs)
426       (error 'pkgusr-unknown-pkg pkg))
427     (with-temp-buffer
428       (insert-file-contents (pkgusr-project-file pkg))
429       (goto-char (point-min))
430       (re-search-forward "Deps: \\(.*$\\)" nil t)
431       (setq deps (match-string 1)))
432     (if (interactive-p)
433         (cond
434          ((or (eq (car current-prefix-arg) 4)
435               (eq (car current-prefix-arg) 64))
436           (insert deps))
437          (t
438           (message "[Deps (%s)]: %s" pkg deps)))
439       (split-string-by-char deps ?\ ))))
440
441 ;;; FIXME: This churns like you wouldn't believe, can we make it more
442 ;;; efficient?
443 (defun pkgusr-pkg-rdeps (&optional pkg upd-list)
444   "Return a list of packages which cite PKG as a dependency.
445
446 With 1 prefix arg, insert into current buffer at point.
447 With 2 prefix args, force update of the packages list.
448 With 3 prefix args, force update and insert into buffer.
449 With optional arg UPD-LIST, force update of the packages list."
450   (interactive)
451   (and (or (eq (car current-prefix-arg) 16)
452            (eq (car current-prefix-arg) 64)
453            upd-list)
454        (pkgusr-all-pkgs-update))
455   (let ((pkg (or pkg (completing-read "Package: "
456                                       (mapcar #'list pkgusr-all-pkgs)
457                                       nil t nil pkgusr-pkg-history)))
458         result)
459     (unless (member pkg pkgusr-all-pkgs)
460       (error 'pkgusr-unknown-pkg pkg))
461     (mapcar
462      #'(lambda (maybe-pkg)
463          (let ((deplist (pkgusr-pkg-deps maybe-pkg)))
464            (mapcar
465             #'(lambda (dep)
466                 (and (string-match pkg dep)
467                      (setq result (append result (list maybe-pkg)))))
468             deplist)))
469      pkgusr-all-pkgs)
470     (sort result #'string<)
471     (if (interactive-p)
472         (cond
473          ((or (eq (car current-prefix-arg) 4)
474               (eq (car current-prefix-arg) 64))
475           (insert (mapconcat #'identity result " ")))
476          (t
477           (message "[Reverse Deps (%s)]: %s" pkg
478                    (mapconcat #'identity result " "))))
479       result)))
480
481 ;; A little bogus perhaps, but it works.  `pkgusr-find-file' is
482 ;; something that only I can use because it ssh's through root to
483 ;; get to the pkgusr. And nobody but me would have a need for
484 ;; `pkgusr-file-history' --SY.
485 (defconst pkgusr-pkgmgr "steve"
486   "The Package Manager.
487
488 This is a defconst for a reason... to make it a bit harder to customise.
489 Just setq'ing this in your init.el won't work if you load pkgusr.el
490 after the setq.  Be bold and hard code it in pkgusr.el itself.")
491
492 (defmacro defun-when-pkgmgr (&rest args)
493   "Define a function only if you are the right user."
494   `(when (equal (user-login-name) pkgusr-pkgmgr)
495      (defun ,@args)))
496
497 (defmacro defvar-when-pkgmgr (&rest args)
498   "Define a variable only if you are the right user."
499   `(when (equal (user-login-name) pkgusr-pkgmgr)
500      (defvar ,@args)))
501
502 (defvar-when-pkgmgr pkgusr-file-history nil
503   "History for pkgusr-find-file.")
504
505 (defun-when-pkgmgr pkgusr-find-file (&optional pkgusr file)
506   "Using Tramp, find PKGUSR's FILE.
507
508 With a prefix arg, force update of the packages list."
509   (interactive)
510   (unless (interactive-p)
511     (error 'invalid-operation "Trying to call interactive-only command"))
512   (and current-prefix-arg
513        (pkgusr-all-pkgs-update))
514   (let* ((puser (completing-read "Package User: "
515                                  (mapcar #'list pkgusr-all-pkgs)
516                                  nil t nil pkgusr-pkg-history))
517          (file (read-file-name (format "[%s] find file: " puser)
518                                (file-name-as-directory
519                                 (expand-file-name puser "/usr/src"))
520                                (file-name-as-directory
521                                 (expand-file-name puser "/usr/src"))
522                                nil nil pkgusr-file-history))
523          (tpath (format "[ssh/root@localhost|su/%s@localhost]%s"
524                         puser file))
525          (default-directory "/"))
526     (find-file tpath)))
527
528 ;; Some key bindings
529 (global-set-key [(hyper c) c] #'pkgusr-cmd-pkg)
530 (global-set-key [(hyper c) f] #'pkgusr-file-pkg)
531 (global-set-key [(hyper c) (hyper r)] #'pkgusr-list-pkgs-regexp)
532 (global-set-key [(hyper c) d] #'pkgusr-pkg-description)
533 (global-set-key [(hyper c) D] #'pkgusr-pkg-deps)
534 (global-set-key [(hyper c) g] #'pkgusr-pkg-general-notes)
535 (global-set-key [(hyper c) i] #'pkgusr-pkg-install-notes)
536 (global-set-key [(hyper c) r] #'pkgusr-pkg-repo)
537 (global-set-key [(hyper c) R] #'pkgusr-pkg-rdeps)
538 (global-set-key [(hyper c) u] #'pkgusr-pkg-url)
539 (global-set-key [(hyper c) v] #'pkgusr-pkg-version)
540 (global-set-key [(hyper c) s] #'pkgusr-show-pkg)
541
542 (eval-and-compile
543   (when (equal (user-login-name) pkgusr-pkgmgr)
544     (global-set-key [(hyper x) (hyper f)] #'pkgusr-find-file)))
545
546 (provide 'pkgusr)
547 ;;; pkgusr.el ends here