Some long overdue updates
[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: <Monday Mar 17, 2014 22:33:38 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 ((lst (with-temp-buffer
66                (insert-file-contents "/etc/group")
67                (re-search-forward "^install:x:9999:" nil t)
68                (narrow-to-region (point) (point-at-eol))
69                (split-string-by-char (buffer-string) ?,))))
70     lst))
71
72 (defvar pkgusr-all-pkgs (pkgusr-all-pkgs)
73   "A list of all installed packages.")
74
75 (defun pkgusr-all-pkgs-update ()
76   "Update the list of installed packages."
77   (setq pkgusr-all-pkgs (pkgusr-all-pkgs)))
78
79 (defun pkgusr-pkgs-count (&optional upd-list)
80   "Return the number of installed packages.
81
82 With optional argument UPD-LIST force an update of the packages list."
83   (and upd-list
84        (pkgusr-all-pkgs-update))
85   (length pkgusr-all-pkgs))
86
87 (defun pkgusr-list-pkgs-regexp (regexp &optional upd-list)
88   "Return a list of packages matching REGEXP.
89
90 With 1 prefix arg  insert the result into the current buffer at point.
91 With 2 prefix args force update of the packages list.
92 With 3 prefix args force update and insert into buffer.
93
94 With optional argument UPD-LIST, force update of the packages list."
95   (interactive "sRegexp: \np")
96   (and (or (eq upd-list 16)
97            (eq upd-list 64))
98        (pkgusr-all-pkgs-update))
99   (let ((case-fold-search t)
100         res)
101     (mapcar
102      #'(lambda (pkg)
103          (when (string-match regexp pkg)
104            (setq res (append res (list pkg)))))
105      pkgusr-all-pkgs)
106     (if (interactive-p)
107         (cond
108          ((or (eq upd-list 4)
109               (eq upd-list 64))
110           (insert (mapconcat #'identity res " ")))
111          (t
112           (message "[Matches for \"%s\"]: %s" regexp
113                    (mapconcat #'identity res " "))))
114       res)))
115
116 (defconst pkgusr-url-regexp
117   (concat
118    #r"\(\(https?\|ftp\|rsync\|s\(cp\|sh\)\|git\)://\|file:/\|s?news:\|mailto:\)"
119    "[^]\t\n \"'()<>[^`{}]*[^]\t\n \"'()<>[^`{}.,;]+")
120   "A regular expression matching URLs.")
121
122 (defun pkgusr-url-at-point ()
123   "Browse to a URL from the `pkgusr-show-pkg' buffer."
124   (interactive)
125   (when (extentp (extent-at (point)))
126     (browse-url (extent-string (extent-at (point))))))
127
128 (defun pkgusr-url-at-mouse (event)
129   "Browse to a URL at EVENT via the mouse from the `pkgusr-show-pkg' buffer."
130   (interactive "e")
131   (when (extentp (extent-at-event event))
132     (browse-url (extent-string (extent-at-event event)))))
133
134 (defconst pkgusr-ext-map
135   (let* ((map (make-sparse-keymap 'pkgusr-ext-map)))
136     (define-key map [button2] 'pkgusr-url-at-mouse)
137     (define-key map [return] 'pkgusr-url-at-point)
138     map)
139   "A keymap for the extents in the `pkgusr-show-pkg' buffer.")
140
141 (defun pkgusr-make-url-extents ()
142   "Create extent objects for all the URLs in the buffer."
143   (goto-char (point-min))
144   (save-excursion
145     (while (re-search-forward pkgusr-url-regexp nil t)
146       (let ((extent (make-extent (match-beginning 0) (match-end 0)))
147             (echo "RET or Button2 to visit this URL."))
148         (set-extent-property extent 'face 'bold)
149         (set-extent-property extent 'mouse-face 'highlight)
150         (set-extent-property extent 'keymap pkgusr-ext-map)
151         (set-extent-property extent 'help-echo echo)
152         (set-extent-property extent 'balloon-help echo)
153         (set-extent-property extent 'duplicable t)))))
154
155 (defun pkgusr-pkg-details (pkg)
156   (let ((buf (get-buffer-create (format "*Package Details: %s*" pkg)))
157         (detail (shell-command-to-string (format "pinky -l %s" pkg))))
158     (with-current-buffer buf
159       (erase-buffer)
160       (insert detail)
161       (pkgusr-make-url-extents))
162     (push-window-configuration)
163     (pop-to-buffer buf)
164     (view-mode nil
165                #'(lambda (b)
166                    (kill-buffer b)
167                    (pop-window-configuration)))))
168
169 (defun pkgusr-show-pkg ()
170   "Display filelist and other details for a package.
171
172 With a prefix arg, force update of the packages list."
173   (interactive)
174   (and current-prefix-arg
175        (pkgusr-all-pkgs-update))
176   (let ((pkg (completing-read "Show Package: "
177                               (mapcar #'list pkgusr-all-pkgs)
178                               nil t nil pkgusr-pkg-history)))
179     (pkgusr-pkg-details pkg)))
180
181 (defun pkgusr-pkg-install-notes ()
182   "Display the install notes of a package.
183
184 With a prefix arg, force update of the packages list."
185   (interactive)
186   (and current-prefix-arg
187        (pkgusr-all-pkgs-update))
188   (let ((pkg (completing-read "Package: "
189                               (mapcar #'list pkgusr-all-pkgs)
190                               nil t nil pkgusr-pkg-history))
191         start end)
192     (pkgusr-pkg-details pkg)
193     (re-search-forward "^Install Notes:$" nil t)
194     (setq start (point-at-bol))
195     (re-search-forward "^General Notes:$" nil t)
196     (setq end (point-at-bol))
197     (narrow-to-region start end)
198     (goto-char (point-min))))
199
200 (defun pkgusr-pkg-general-notes ()
201   "Display the general notes of a package.
202
203 With a prefix arg, force update of the packages list."
204   (interactive)
205   (and current-prefix-arg
206        (pkgusr-all-pkgs-update))
207   (let ((pkg (completing-read "Package: "
208                               (mapcar #'list pkgusr-all-pkgs)
209                               nil t nil pkgusr-pkg-history))
210         start end)
211     (pkgusr-pkg-details pkg)
212     (re-search-forward "^General Notes:$" nil t)
213     (setq start (point-at-bol))
214     (re-search-forward "^CONTENTS:$" nil t)
215     (setq end (point-at-bol))
216     (narrow-to-region start end)
217     (goto-char (point-min))))
218
219 (defun pkgusr-cmd-pkg (cmd)
220   "Display the package name \(user:group\) which contains CMD.
221
222 With a prefix arg, insert into the current buffer at point.
223
224 If non-interactive, return a list whose car is user and cdr is group."
225   (interactive "sCommand: ")
226   (let* ((cmd (or (executable-find cmd)
227                   (error 'pkgusr-unknown-cmd cmd)))
228          (user (user-login-name
229                 (nth 2 (file-attributes cmd))))
230          (group (or (declare-fboundp (user-group-name
231                                       (nth 3 (file-attributes cmd))))
232                     (user-login-name
233                      (nth 3 (file-attributes cmd))))))
234     (unless (or (member user pkgusr-all-pkgs)
235                 (member group pkgusr-all-pkgs))
236       (error 'pkgusr-unknown-pkg user))
237     (if (interactive-p)
238         (if current-prefix-arg
239             (insert (format "%s: (%s:%s)" cmd user group))
240           (message "Command: %s is from the \"%s\" package (%2$s:%s)"
241                    cmd user group))
242       (list user group))))
243
244 (defun pkgusr-file-pkg (file)
245   "Display the pkg name \(user:group\) which contains FILE.
246
247 With a prefix arg, insert into current buffer at point.
248
249 If non-interactive, return a list whose car is user and cdr is group."
250   (interactive "fFile: ")
251   (let ((user (user-login-name
252                (nth 2 (file-attributes file))))
253         (group (or (declare-fboundp (user-group-name
254                                      (nth 3 (file-attributes file))))
255                    (user-login-name
256                     (nth 3 (file-attributes file))))))
257     (unless (or (member user pkgusr-all-pkgs)
258                 (member group pkgusr-all-pkgs))
259       (error 'pkgusr-unknown-pkg user))
260     (if (interactive-p)
261         (if current-prefix-arg
262             (insert (format "%s: (%s:%s)" file user group))
263           (message "File: %s is from the \"%s\" package (%2$s:%s)"
264                    file user group)
265           (list user group)))))
266
267 (defun pkgusr-project-file (pkg)
268   "Return the .project file for PKG."
269   (let ((dir (paths-construct-path (list "/usr" "src" pkg))))
270     (expand-file-name ".project" dir)))
271
272 (defun pkgusr-pkg-url (&optional pkg upd-list)
273   "Return the URL of PKG as a string.
274
275 With 1 prefix arg, insert into current buffer at point.
276 With 2 prefix args, force update of the packages list.
277 With 3 prefix args, force update and insert into buffer.
278 With optional arg UPD-LIST, force update of the packages list."
279   (interactive)
280   (and (or (eq (car current-prefix-arg) 16)
281            (eq (car current-prefix-arg) 64)
282            upd-list)
283        (pkgusr-all-pkgs-update))
284   (let ((pkg (or pkg
285                  (completing-read "Package: "
286                                   (mapcar #'list pkgusr-all-pkgs)
287                                   nil t nil pkgusr-pkg-history)))
288         url)
289     (unless (member pkg pkgusr-all-pkgs)
290       (error 'pkgusr-unknown-pkg pkg))
291     (with-temp-buffer
292       (insert-file-contents (pkgusr-project-file pkg))
293       (goto-char (point-min))
294       (re-search-forward "Web_Site: <\\(.*\\)>$" nil t)
295       (setq url (match-string 1)))
296     (if (interactive-p)
297         (cond
298          ((or (eq (car current-prefix-arg) 4)
299               (eq (car current-prefix-arg) 64))
300           (insert url))
301          (t
302           (message "[URL (%s)]: %s" pkg url)))
303       url)))
304
305 (defun pkgusr-pkg-repo (&optional pkg upd-list)
306   "Return the repo URI of PKG as a string.
307
308 With 1 prefix arg, insert into current buffer at point.
309 With 2 prefix args, force update of the packages list.
310 With 3 prefix args, force update and insert into buffer.
311 With optional arg UPD-LIST, force update of the packages list."
312   (interactive)
313   (and (or (eq (car current-prefix-arg) 16)
314            (eq (car current-prefix-arg) 64)
315            upd-list)
316        (pkgusr-all-pkgs-update))
317   (let ((pkg (or pkg (completing-read "Package: "
318                                       (mapcar #'list pkgusr-all-pkgs)
319                                       nil t nil pkgusr-pkg-history)))
320         repo type)
321     (unless (member pkg pkgusr-all-pkgs)
322       (error 'pkgusr-unknown-pkg pkg))
323     (with-temp-buffer
324       (insert-file-contents (pkgusr-project-file pkg))
325       (goto-char (point-min))
326       (re-search-forward "Repo_Type: <\\(.*\\)>$" nil t)
327       (setq type (match-string 1))
328       (re-search-forward "Repo_Location: <\\(.*\\)>" nil t)
329       (setq repo (match-string 1)))
330     (if (interactive-p)
331         (cond
332          ((or (eq (car current-prefix-arg) 4)
333               (eq (car current-prefix-arg) 64))
334           (insert repo))
335          (t
336           (message "[Repo (%s)]: %s (%s)" pkg repo type)))
337       repo)))
338
339 (defun pkgusr-pkg-version (&optional pkg upd-list)
340   "Return the version of PKG as a string.
341
342 With 1 prefix arg, insert into current buffer at point.
343 With 2 prefix args, force update of the packages list.
344 With 3 prefix args, force update and insert into buffer.
345 With optional arg UPD-LIST, force update of the packages list."
346   (interactive)
347   (and (or (eq (car current-prefix-arg) 16)
348            (eq (car current-prefix-arg) 64)
349            upd-list)
350        (pkgusr-all-pkgs-update))
351   (let ((pkg (or pkg (completing-read "Package: "
352                                       (mapcar #'list pkgusr-all-pkgs)
353                                       nil t nil pkgusr-pkg-history)))
354         version)
355     (unless (member pkg pkgusr-all-pkgs)
356       (error 'pkgusr-unknown-pkg pkg))
357     (with-temp-buffer
358       (insert-file-contents (pkgusr-project-file pkg))
359       (goto-char (point-min))
360       (re-search-forward "Version: \\(.*$\\)" nil t)
361       (setq version (match-string 1)))
362     (if (interactive-p)
363         (cond
364          ((or (eq (car current-prefix-arg) 4)
365               (eq (car current-prefix-arg) 64))
366           (insert version))
367          (t
368           (message "[Version (%s)]: %s" pkg version)))
369       version)))
370
371 (defun pkgusr-pkg-description (&optional pkg upd-list)
372   "Return the description of PKG as a string.
373
374 With 1 prefix arg, insert into current buffer at point.
375 With 2 prefix args, force update of the packages list.
376 With 3 prefix args, force update and insert into buffer.
377 With optional arg UPD-LIST, force update of the packages list."
378   (interactive)
379   (and (or (eq (car current-prefix-arg) 16)
380            (eq (car current-prefix-arg) 64)
381            upd-list)
382        (pkgusr-all-pkgs-update))
383   (let ((pkg (or pkg (completing-read "Package: "
384                                       (mapcar #'list pkgusr-all-pkgs)
385                                       nil t nil pkgusr-pkg-history)))
386         desc)
387     (unless (member pkg pkgusr-all-pkgs)
388       (error 'pkgusr-unknown-pkg pkg))
389     (with-temp-buffer
390       (insert-file-contents (pkgusr-project-file pkg))
391       (goto-char (point-min))
392       (re-search-forward "Description: \\(.*$\\)" nil t)
393       (setq desc (match-string 1)))
394     (if (interactive-p)
395         (cond
396          ((or (eq (car current-prefix-arg) 4)
397               (eq (car current-prefix-arg) 64))
398           (insert desc))
399          (t
400           (message "[Description (%s)]: %s" pkg desc)))
401       desc)))
402
403 (defun pkgusr-pkg-deps (&optional pkg upd-list)
404   "Return a list of dependencies of PKG.
405
406 With 1 prefix arg, insert into current buffer at point.
407 With 2 prefix args, force update of the packages list.
408 With 3 prefix args, force update and insert into buffer.
409 With optional arg UPD-LIST, force update of the packages list."
410   (interactive)
411   (and (or (eq (car current-prefix-arg) 16)
412            (eq (car current-prefix-arg) 64)
413            upd-list)
414        (pkgusr-all-pkgs-update))
415   (let ((pkg (or pkg (completing-read "Package: "
416                                       (mapcar #'list pkgusr-all-pkgs)
417                                       nil t nil pkgusr-pkg-history)))
418         deps)
419     (unless (member pkg pkgusr-all-pkgs)
420       (error 'pkgusr-unknown-pkg pkg))
421     (with-temp-buffer
422       (insert-file-contents (pkgusr-project-file pkg))
423       (goto-char (point-min))
424       (re-search-forward "Deps: \\(.*$\\)" nil t)
425       (setq deps (match-string 1)))
426     (if (interactive-p)
427         (cond
428          ((or (eq (car current-prefix-arg) 4)
429               (eq (car current-prefix-arg) 64))
430           (insert deps))
431          (t
432           (message "[Deps (%s)]: %s" pkg deps)))
433       (split-string-by-char deps ?\ ))))
434
435 ;;; FIXME: This churns like you wouldn't believe, can we make it more
436 ;;; efficient?
437 (defun pkgusr-pkg-rdeps (&optional pkg upd-list)
438   "Return a list of packages which cite PKG as a dependency.
439
440 With 1 prefix arg, insert into current buffer at point.
441 With 2 prefix args, force update of the packages list.
442 With 3 prefix args, force update and insert into buffer.
443 With optional arg UPD-LIST, force update of the packages list."
444   (interactive)
445   (and (or (eq (car current-prefix-arg) 16)
446            (eq (car current-prefix-arg) 64)
447            upd-list)
448        (pkgusr-all-pkgs-update))
449   (let ((pkg (or pkg (completing-read "Package: "
450                                       (mapcar #'list pkgusr-all-pkgs)
451                                       nil t nil pkgusr-pkg-history)))
452         result)
453     (unless (member pkg pkgusr-all-pkgs)
454       (error 'pkgusr-unknown-pkg pkg))
455     (mapcar
456      #'(lambda (maybe-pkg)
457          (let ((deplist (pkgusr-pkg-deps maybe-pkg)))
458            (mapcar
459             #'(lambda (dep)
460                 (and (string-match pkg dep)
461                      (setq result (append result (list maybe-pkg)))))
462             deplist)))
463      pkgusr-all-pkgs)
464     (sort result #'string<)
465     (if (interactive-p)
466         (cond
467          ((or (eq (car current-prefix-arg) 4)
468               (eq (car current-prefix-arg) 64))
469           (insert (mapconcat #'identity result " ")))
470          (t
471           (message "[Reverse Deps (%s)]: %s" pkg
472                    (mapconcat #'identity result " "))))
473       result)))
474
475 ;; A little bogus perhaps, but it works.  `pkgusr-find-file' is
476 ;; something that only I can use because it ssh's through root to
477 ;; get to the pkgusr. And nobody but me would have a need for
478 ;; `pkgusr-file-history' --SY.
479 (defconst pkgusr-pkgmgr "steve"
480   "The Package Manager.
481
482 This is a defconst for a reason... to make it a bit harder to customise.
483 Just setq'ing this in your init.el won't work if you load pkgusr.el
484 after the setq.  Be bold and hard code it in pkgusr.el itself.")
485
486 (defmacro defun-when-pkgmgr (&rest args)
487   "Define a function only if you are the right user."
488   `(when (equal (user-login-name) pkgusr-pkgmgr)
489      (defun ,@args)))
490
491 (defmacro defvar-when-pkgmgr (&rest args)
492   "Define a variable only if you are the right user."
493   `(when (equal (user-login-name) pkgusr-pkgmgr)
494      (defvar ,@args)))
495
496 (defvar-when-pkgmgr pkgusr-file-history nil
497   "History for pkgusr-find-file.")
498
499 (defun-when-pkgmgr pkgusr-find-file (&optional pkgusr file)
500   "Using Tramp, find PKGUSR's FILE.
501
502 With a prefix arg, force update of the packages list."
503   (interactive)
504   (unless (interactive-p)
505     (error 'invalid-operation "Trying to call interactive-only command"))
506   (and current-prefix-arg
507        (pkgusr-all-pkgs-update))
508   (let* ((puser (completing-read "Package User: "
509                                  (mapcar #'list pkgusr-all-pkgs)
510                                  nil t nil pkgusr-pkg-history))
511          (file (read-file-name (format "[%s] find file: " puser)
512                                (file-name-as-directory
513                                 (expand-file-name puser "/usr/src"))
514                                (file-name-as-directory
515                                 (expand-file-name puser "/usr/src"))
516                                nil nil pkgusr-file-history))
517          (tpath (format "[ssh/root@localhost|su/%s@localhost]%s"
518                         puser file))
519          (default-directory "/"))
520     (find-file tpath)))
521
522 ;; Some key bindings
523 (global-set-key [(hyper c) c] #'pkgusr-cmd-pkg)
524 (global-set-key [(hyper c) f] #'pkgusr-file-pkg)
525 (global-set-key [(hyper c) (hyper r)] #'pkgusr-list-pkgs-regexp)
526 (global-set-key [(hyper c) d] #'pkgusr-pkg-description)
527 (global-set-key [(hyper c) D] #'pkgusr-pkg-deps)
528 (global-set-key [(hyper c) g] #'pkgusr-pkg-general-notes)
529 (global-set-key [(hyper c) i] #'pkgusr-pkg-install-notes)
530 (global-set-key [(hyper c) r] #'pkgusr-pkg-repo)
531 (global-set-key [(hyper c) R] #'pkgusr-pkg-rdeps)
532 (global-set-key [(hyper c) u] #'pkgusr-pkg-url)
533 (global-set-key [(hyper c) v] #'pkgusr-pkg-version)
534 (global-set-key [(hyper c) s] #'pkgusr-show-pkg)
535
536 (eval-and-compile
537   (when (equal (user-login-name) pkgusr-pkgmgr)
538     (global-set-key [(hyper x) (hyper f)] #'pkgusr-find-file)))
539
540 (provide 'pkgusr)
541 ;;; pkgusr.el ends here