1#!/bin/sh
2# This is actually -*- mode: scheme; coding: utf-8; -*- text.
3main='(module-ref (resolve-module '\''(gnupdate)) '\'gnupdate')'
4exec ${GUILE-guile} -L "$PWD" -l "$0" \
5 -c "(apply $main (command-line))" "$@"
6!#
7;;; GNUpdate -- Update GNU packages in Nixpkgs.
8;;; Copyright (C) 2010, 2011 Ludovic Courtès <ludo@gnu.org>
9;;;
10;;; This program is free software: you can redistribute it and/or modify
11;;; it under the terms of the GNU General Public License as published by
12;;; the Free Software Foundation, either version 3 of the License, or
13;;; (at your option) any later version.
14;;;
15;;; This program is distributed in the hope that it will be useful,
16;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;;; GNU General Public License for more details.
19;;;
20;;; You should have received a copy of the GNU General Public License
21;;; along with this program. If not, see <http://www.gnu.org/licenses/>.
22
23(cond-expand (guile-2 #t)
24 (else (error "GNU Guile 2.0 is required")))
25
26(define-module (gnupdate)
27 #:use-module (sxml ssax)
28 #:use-module (ice-9 popen)
29 #:use-module (ice-9 match)
30 #:use-module (ice-9 rdelim)
31 #:use-module (ice-9 format)
32 #:use-module (ice-9 regex)
33 #:use-module (ice-9 vlist)
34 #:use-module (srfi srfi-1)
35 #:use-module (srfi srfi-9)
36 #:use-module (srfi srfi-11)
37 #:use-module (srfi srfi-26)
38 #:use-module (srfi srfi-37)
39 #:use-module (system foreign)
40 #:use-module (rnrs bytevectors)
41 #:export (gnupdate))
42
43
44;;;
45;;; SNix.
46;;;
47
48(define-record-type <location>
49 (make-location file line column)
50 location?
51 (file location-file)
52 (line location-line)
53 (column location-column))
54
55(define (->loc line column path)
56 (and line column path
57 (make-location path (string->number line) (string->number column))))
58
59;; Nix object types visible in the XML output of `nix-instantiate' and
60;; mapping to S-expressions (we map to sexps, not records, so that we
61;; can do pattern matching):
62;;
63;; at (at varpat attrspat)
64;; attr (attribute loc name value)
65;; attrs (attribute-set attributes)
66;; attrspat (attribute-set-pattern patterns)
67;; bool #f|#t
68;; derivation (derivation drv-path out-path attributes)
69;; ellipsis '...
70;; expr (snix loc body ...)
71;; function (function loc at|attrspat|varpat)
72;; int int
73;; list list
74;; null 'null
75;; path string
76;; string string
77;; unevaluated 'unevaluated
78;; varpat (varpat name)
79;;
80;; Initially ATTRIBUTES in `derivation' and `attribute-set' was a promise;
81;; however, handling `repeated' nodes makes it impossible to do anything
82;; lazily because the whole SXML tree has to be traversed to maintain the
83;; list of known derivations.
84
85(define (xml-element->snix elem attributes body derivations)
86 ;; Return an SNix element corresponding to XML element ELEM.
87
88 (define (loc)
89 (->loc (assq-ref attributes 'line)
90 (assq-ref attributes 'column)
91 (assq-ref attributes 'path)))
92
93 (case elem
94 ((at)
95 (values `(at ,(car body) ,(cadr body)) derivations))
96 ((attr)
97 (let ((name (assq-ref attributes 'name)))
98 (cond ((null? body)
99 (values `(attribute-pattern ,name) derivations))
100 ((and (pair? body) (null? (cdr body)))
101 (values `(attribute ,(loc) ,name ,(car body))
102 derivations))
103 (else
104 (error "invalid attribute body" name (loc) body)))))
105 ((attrs)
106 (values `(attribute-set ,(reverse body)) derivations))
107 ((attrspat)
108 (values `(attribute-set-pattern ,body) derivations))
109 ((bool)
110 (values (string-ci=? "true" (assq-ref attributes 'value))
111 derivations))
112 ((derivation)
113 (let ((drv-path (assq-ref attributes 'drvPath))
114 (out-path (assq-ref attributes 'outPath)))
115 (if (equal? body '(repeated))
116 (let ((body (vhash-assoc drv-path derivations)))
117 (if (pair? body)
118 (values `(derivation ,drv-path ,out-path ,(cdr body))
119 derivations)
120
121 ;; DRV-PATH hasn't been encountered yet but may be later
122 ;; (see <http://article.gmane.org/gmane.linux.distributions.nixos/5946>.)
123 ;; Return an `unresolved' node.
124 (values `(unresolved
125 ,(lambda (derivations)
126 (let ((body (vhash-assoc drv-path derivations)))
127 (if (pair? body)
128 `(derivation ,drv-path ,out-path
129 ,(cdr body))
130 (error "no previous occurrence of derivation"
131 drv-path)))))
132 derivations)))
133 (values `(derivation ,drv-path ,out-path ,body)
134 (vhash-cons drv-path body derivations)))))
135 ((ellipsis)
136 (values '... derivations))
137 ((expr)
138 (values `(snix ,(loc) ,@body) derivations))
139 ((function)
140 (values `(function ,(loc) ,body) derivations))
141 ((int)
142 (values (string->number (assq-ref attributes 'value))
143 derivations))
144 ((list)
145 (values body derivations))
146 ((null)
147 (values 'null derivations))
148 ((path)
149 (values (assq-ref attributes 'value) derivations))
150 ((repeated)
151 (values 'repeated derivations))
152 ((string)
153 (values (assq-ref attributes 'value) derivations))
154 ((unevaluated)
155 (values 'unevaluated derivations))
156 ((varpat)
157 (values `(varpat ,(assq-ref attributes 'name)) derivations))
158 (else (error "unhandled Nix XML element" elem))))
159
160(define (resolve snix derivations)
161 "Return a new SNix tree where `unresolved' nodes from SNIX have been
162replaced by the result of their application to DERIVATIONS, a vhash."
163 (let loop ((node snix)
164 (seen vlist-null))
165 (if (vhash-assq node seen)
166 (values node seen)
167 (match node
168 (('unresolved proc)
169 (let ((n (proc derivations)))
170 (values n seen)))
171 ((tag body ...)
172 (let ((body+seen (fold (lambda (n body+seen)
173 (call-with-values
174 (lambda ()
175 (loop n (cdr body+seen)))
176 (lambda (n* seen)
177 (cons (cons n* (car body+seen))
178 (vhash-consq n #t seen)))))
179 (cons '() (vhash-consq node #t seen))
180 body)))
181 (values (cons tag (reverse (car body+seen)))
182 (vhash-consq node #t (cdr body+seen)))))
183 (anything
184 (values anything seen))))))
185
186(define xml->snix
187 ;; Return the SNix represention of TREE, an SXML tree as returned by
188 ;; parsing the XML output of `nix-instantiate' on Nixpkgs.
189 (let ((parse
190 (ssax:make-parser NEW-LEVEL-SEED
191 (lambda (elem-gi attributes namespaces expected-content
192 seed)
193 (cons '() (cdr seed)))
194
195 FINISH-ELEMENT
196 (lambda (elem-gi attributes namespaces parent-seed
197 seed)
198 (let ((snix (car seed))
199 (derivations (cdr seed)))
200 (let-values (((snix derivations)
201 (xml-element->snix elem-gi
202 attributes
203 snix
204 derivations)))
205 (cons (cons snix (car parent-seed))
206 derivations))))
207
208 CHAR-DATA-HANDLER
209 (lambda (string1 string2 seed)
210 ;; Discard inter-node strings, which are blanks.
211 seed))))
212 (lambda (port)
213 (match (parse port (cons '() vlist-null))
214 (((snix) . derivations)
215 (resolve snix derivations))))))
216
217(define (call-with-package snix proc)
218 (match snix
219 (('attribute _ (and attribute-name (? string?))
220 ('derivation _ _ body))
221 ;; Ugly pattern matching.
222 (let ((meta
223 (any (lambda (attr)
224 (match attr
225 (('attribute _ "meta" ('attribute-set metas)) metas)
226 (_ #f)))
227 body))
228 (package-name
229 (any (lambda (attr)
230 (match attr
231 (('attribute _ "name" (and name (? string?)))
232 name)
233 (_ #f)))
234 body))
235 (location
236 (any (lambda (attr)
237 (match attr
238 (('attribute loc "name" (? string?))
239 loc)
240 (_ #f)))
241 body))
242 (src
243 (any (lambda (attr)
244 (match attr
245 (('attribute _ "src" src)
246 src)
247 (_ #f)))
248 body)))
249 (proc attribute-name package-name location meta src)))))
250
251(define (call-with-src snix proc)
252 ;; Assume SNIX contains the SNix expression for the value of an `src'
253 ;; attribute, as returned by `call-with-package', and call PROC with the
254 ;; relevant SRC information, or #f if SNIX doesn't match.
255 (match snix
256 (('derivation _ _ body)
257 (let ((name
258 (any (lambda (attr)
259 (match attr
260 (('attribute _ "name" (and name (? string?)))
261 name)
262 (_ #f)))
263 body))
264 (output-hash
265 (any (lambda (attr)
266 (match attr
267 (('attribute _ "outputHash" (and hash (? string?)))
268 hash)
269 (_ #f)))
270 body))
271 (urls
272 (any (lambda (attr)
273 (match attr
274 (('attribute _ "urls" (and urls (? pair?)))
275 urls)
276 (_ #f)))
277 body)))
278 (proc name output-hash urls)))
279 (_ (proc #f #f #f))))
280
281(define (src->values snix)
282 (call-with-src snix values))
283
284(define (attribute-value attribute)
285 ;; Return the value of ATTRIBUTE.
286 (match attribute
287 (('attribute _ _ value) value)))
288
289(define (derivation-source derivation)
290 ;; Return the "src" attribute of DERIVATION or #f if not found.
291 (match derivation
292 (('derivation _ _ (attributes ...))
293 (find-attribute-by-name "src" attributes))))
294
295(define (derivation-output-path derivation)
296 ;; Return the output path of DERIVATION.
297 (match derivation
298 (('derivation _ out-path _)
299 out-path)
300 (_ #f)))
301
302(define (source-output-path src)
303 ;; Return the output path of SRC, the "src" attribute of a derivation.
304 (derivation-output-path (attribute-value src)))
305
306(define (derivation-source-output-path derivation)
307 ;; Return the output path of the "src" attribute of DERIVATION or #f if
308 ;; DERIVATION lacks an "src" attribute.
309 (and=> (derivation-source derivation) source-output-path))
310
311(define* (open-nixpkgs nixpkgs #:optional attribute)
312 ;; Return an input pipe to the XML representation of Nixpkgs. When
313 ;; ATTRIBUTE is true, only that attribute is considered.
314 (let ((script (string-append nixpkgs
315 "/maintainers/scripts/eval-release.nix")))
316 (apply open-pipe* OPEN_READ
317 "nix-instantiate" "--strict" "--eval-only" "--xml"
318 `(,@(if attribute
319 `("-A" ,attribute)
320 '())
321 ,script))))
322
323(define (pipe-failed? pipe)
324 "Close pipe and return its status if it failed."
325 (let ((status (close-pipe pipe)))
326 (if (or (status:term-sig status)
327 (not (= (status:exit-val status) 0)))
328 status
329 #f)))
330
331(define (memoize proc)
332 "Return a memoizing version of PROC."
333 (let ((cache (make-hash-table)))
334 (lambda args
335 (let ((results (hash-ref cache args)))
336 (if results
337 (apply values results)
338 (let ((results (call-with-values (lambda ()
339 (apply proc args))
340 list)))
341 (hash-set! cache args results)
342 (apply values results)))))))
343
344(define nix-prefetch-url
345 (memoize
346 (lambda (url)
347 "Download URL in the Nix store and return the base32-encoded SHA256 hash of
348the file at URL."
349 (let* ((pipe (open-pipe* OPEN_READ "nix-prefetch-url" url))
350 (hash (read-line pipe)))
351 (if (or (pipe-failed? pipe)
352 (eof-object? hash))
353 (values #f #f)
354 (let* ((pipe (open-pipe* OPEN_READ "nix-store" "--print-fixed-path"
355 "sha256" hash (basename url)))
356 (path (read-line pipe)))
357 (if (or (pipe-failed? pipe)
358 (eof-object? path))
359 (values #f #f)
360 (values (string-trim-both hash) (string-trim-both path)))))))))
361
362(define (update-nix-expression file
363 old-version old-hash
364 new-version new-hash)
365 ;; Modify FILE in-place. Ugly: we call out to sed(1).
366 (let ((cmd (format #f "sed -i \"~a\" -e 's/~A/~a/g ; s/~A/~A/g'"
367 file
368 (regexp-quote old-version) new-version
369 old-hash
370 (or new-hash "new hash not available, check the log"))))
371 (format #t "running `~A'...~%" cmd)
372 (system cmd)))
373
374(define (find-attribute-by-name name attributes)
375 ;; Return attribute NAME in ATTRIBUTES, a list of SNix attributes, or #f if
376 ;; NAME cannot be found.
377 (find (lambda (a)
378 (match a
379 (('attribute _ (? (cut string=? <> name)) _)
380 a)
381 (_ #f)))
382 attributes))
383
384(define (find-package-by-attribute-name name packages)
385 ;; Return the package bound to attribute NAME in PACKAGES, a list of
386 ;; packages (SNix attributes), or #f if NAME cannot be found.
387 (find (lambda (package)
388 (match package
389 (('attribute _ (? (cut string=? <> name))
390 ('derivation _ _ _))
391 package)
392 (_ #f)))
393 packages))
394
395(define (stdenv-package packages)
396 ;; Return the `stdenv' package from PACKAGES, a list of SNix attributes.
397 (find-package-by-attribute-name "stdenv" packages))
398
399(define (package-requisites package)
400 ;; Return the list of derivations required to build PACKAGE (including that
401 ;; of PACKAGE) by recurring into its derivation attributes.
402 (let loop ((snix package)
403 (result '()))
404 (match snix
405 (('attribute _ _ body)
406 (loop body result))
407 (('derivation _ out-path body)
408 (if (any (lambda (d)
409 (match d
410 (('derivation _ (? (cut string=? out-path <>)) _) #t)
411 (_ #f)))
412 result)
413 result
414 (loop body (cons snix result))))
415 ((things ...)
416 (fold loop result things))
417 (_ result))))
418
419(define (package-source-output-path package)
420 ;; Return the output path of the "src" derivation of PACKAGE.
421 (derivation-source-output-path (attribute-value package)))
422
423
424;;;
425;;; GnuPG interface.
426;;;
427
428(define %gpg-command "gpg2")
429(define %openpgp-key-server "keys.gnupg.net")
430
431(define (gnupg-verify sig file)
432 "Verify signature SIG for FILE. Return a status s-exp if GnuPG failed."
433
434 (define (status-line->sexp line)
435 ;; See file `doc/DETAILS' in GnuPG.
436 (define sigid-rx
437 (make-regexp
438 "^\\[GNUPG:\\] SIG_ID ([A-Za-z0-9/]+) ([[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}) ([[:digit:]]+)"))
439 (define goodsig-rx
440 (make-regexp "^\\[GNUPG:\\] GOODSIG ([[:xdigit:]]+) (.+)$"))
441 (define validsig-rx
442 (make-regexp
443 "^\\[GNUPG:\\] VALIDSIG ([[:xdigit:]]+) ([[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}) ([[:digit:]]+) .*$"))
444 (define expkeysig-rx ; good signature, but expired key
445 (make-regexp "^\\[GNUPG:\\] EXPKEYSIG ([[:xdigit:]]+) (.*)$"))
446 (define errsig-rx
447 (make-regexp
448 "^\\[GNUPG:\\] ERRSIG ([[:xdigit:]]+) ([^ ]+) ([^ ]+) ([^ ]+) ([[:digit:]]+) ([[:digit:]]+)"))
449
450 (cond ((regexp-exec sigid-rx line)
451 =>
452 (lambda (match)
453 `(signature-id ,(match:substring match 1) ; sig id
454 ,(match:substring match 2) ; date
455 ,(string->number ; timestamp
456 (match:substring match 3)))))
457 ((regexp-exec goodsig-rx line)
458 =>
459 (lambda (match)
460 `(good-signature ,(match:substring match 1) ; key id
461 ,(match:substring match 2)))) ; user name
462 ((regexp-exec validsig-rx line)
463 =>
464 (lambda (match)
465 `(valid-signature ,(match:substring match 1) ; fingerprint
466 ,(match:substring match 2) ; sig creation date
467 ,(string->number ; timestamp
468 (match:substring match 3)))))
469 ((regexp-exec expkeysig-rx line)
470 =>
471 (lambda (match)
472 `(expired-key-signature ,(match:substring match 1) ; fingerprint
473 ,(match:substring match 2)))) ; user name
474 ((regexp-exec errsig-rx line)
475 =>
476 (lambda (match)
477 `(signature-error ,(match:substring match 1) ; key id or fingerprint
478 ,(match:substring match 2) ; pubkey algo
479 ,(match:substring match 3) ; hash algo
480 ,(match:substring match 4) ; sig class
481 ,(string->number ; timestamp
482 (match:substring match 5))
483 ,(let ((rc
484 (string->number ; return code
485 (match:substring match 6))))
486 (case rc
487 ((9) 'missing-key)
488 ((4) 'unknown-algorithm)
489 (else rc))))))
490 (else
491 `(unparsed-line ,line))))
492
493 (define (parse-status input)
494 (let loop ((line (read-line input))
495 (result '()))
496 (if (eof-object? line)
497 (reverse result)
498 (loop (read-line input)
499 (cons (status-line->sexp line) result)))))
500
501 (let* ((pipe (open-pipe* OPEN_READ %gpg-command "--status-fd=1"
502 "--verify" sig file))
503 (status (parse-status pipe)))
504 ;; Ignore PIPE's exit status since STATUS above should contain all the
505 ;; info we need.
506 (close-pipe pipe)
507 status))
508
509(define (gnupg-status-good-signature? status)
510 "If STATUS, as returned by `gnupg-verify', denotes a good signature, return
511a key-id/user pair; return #f otherwise."
512 (any (lambda (sexp)
513 (match sexp
514 (((or 'good-signature 'expired-key-signature) key-id user)
515 (cons key-id user))
516 (_ #f)))
517 status))
518
519(define (gnupg-status-missing-key? status)
520 "If STATUS denotes a missing-key error, then return the key-id of the
521missing key."
522 (any (lambda (sexp)
523 (match sexp
524 (('signature-error key-id _ ...)
525 key-id)
526 (_ #f)))
527 status))
528
529(define (gnupg-receive-keys key-id)
530 (system* %gpg-command "--keyserver" %openpgp-key-server "--recv-keys" key-id))
531
532(define (gnupg-verify* sig file)
533 "Like `gnupg-verify', but try downloading the public key if it's missing.
534Return #t if the signature was good, #f otherwise."
535 (let ((status (gnupg-verify sig file)))
536 (or (gnupg-status-good-signature? status)
537 (let ((missing (gnupg-status-missing-key? status)))
538 (and missing
539 (begin
540 ;; Download the missing key and try again.
541 (gnupg-receive-keys missing)
542 (gnupg-status-good-signature? (gnupg-verify sig file))))))))
543
544
545;;;
546;;; FTP client.
547;;;
548
549(define-record-type <ftp-connection>
550 (%make-ftp-connection socket addrinfo)
551 ftp-connection?
552 (socket ftp-connection-socket)
553 (addrinfo ftp-connection-addrinfo))
554
555(define %ftp-ready-rx
556 (make-regexp "^([0-9]{3}) (.+)$"))
557
558(define (%ftp-listen port)
559 (let loop ((line (read-line port)))
560 (cond ((eof-object? line) (values line #f))
561 ((regexp-exec %ftp-ready-rx line)
562 =>
563 (lambda (match)
564 (values (string->number (match:substring match 1))
565 (match:substring match 2))))
566 (else
567 (loop (read-line port))))))
568
569(define (%ftp-command command expected-code port)
570 (format port "~A~A~A" command (string #\return) (string #\newline))
571 (let-values (((code message) (%ftp-listen port)))
572 (if (eqv? code expected-code)
573 message
574 (throw 'ftp-error port command code message))))
575
576(define (%ftp-login user pass port)
577 (let ((command (string-append "USER " user (string #\newline))))
578 (display command port)
579 (let-values (((code message) (%ftp-listen port)))
580 (case code
581 ((230) #t)
582 ((331) (%ftp-command (string-append "PASS " pass) 230 port))
583 (else (throw 'ftp-error port command code message))))))
584
585(define (ftp-open host)
586 (catch 'getaddrinfo-error
587 (lambda ()
588 (let* ((ai (car (getaddrinfo host "ftp")))
589 (s (socket (addrinfo:fam ai) (addrinfo:socktype ai)
590 (addrinfo:protocol ai))))
591 (connect s (addrinfo:addr ai))
592 (setvbuf s _IOLBF)
593 (let-values (((code message) (%ftp-listen s)))
594 (if (eqv? code 220)
595 (begin
596 ;(%ftp-command "OPTS UTF8 ON" 200 s)
597 (%ftp-login "anonymous" "ludo@example.com" s)
598 (%make-ftp-connection s ai))
599 (begin
600 (format (current-error-port) "FTP to `~a' failed: ~A: ~A~%"
601 host code message)
602 (close s)
603 #f)))))
604 (lambda (key errcode)
605 (format (current-error-port) "failed to resolve `~a': ~a~%"
606 host (gai-strerror errcode))
607 #f)))
608
609(define (ftp-close conn)
610 (close (ftp-connection-socket conn)))
611
612(define (ftp-chdir conn dir)
613 (%ftp-command (string-append "CWD " dir) 250
614 (ftp-connection-socket conn)))
615
616(define (ftp-pasv conn)
617 (define %pasv-rx
618 (make-regexp "([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)"))
619
620 (let ((message (%ftp-command "PASV" 227 (ftp-connection-socket conn))))
621 (cond ((regexp-exec %pasv-rx message)
622 =>
623 (lambda (match)
624 (+ (* (string->number (match:substring match 5)) 256)
625 (string->number (match:substring match 6)))))
626 (else
627 (throw 'ftp-error conn "PASV" 227 message)))))
628
629
630(define* (ftp-list conn #:optional directory)
631 (define (address-with-port sa port)
632 (let ((fam (sockaddr:fam sa))
633 (addr (sockaddr:addr sa)))
634 (cond ((= fam AF_INET)
635 (make-socket-address fam addr port))
636 ((= fam AF_INET6)
637 (make-socket-address fam addr port
638 (sockaddr:flowinfo sa)
639 (sockaddr:scopeid sa)))
640 (else #f))))
641
642 (if directory
643 (ftp-chdir conn directory))
644
645 (let* ((port (ftp-pasv conn))
646 (ai (ftp-connection-addrinfo conn))
647 (s (socket (addrinfo:fam ai) (addrinfo:socktype ai)
648 (addrinfo:protocol ai))))
649 (connect s (address-with-port (addrinfo:addr ai) port))
650 (setvbuf s _IOLBF)
651
652 (dynamic-wind
653 (lambda () #t)
654 (lambda ()
655 (%ftp-command "LIST" 150 (ftp-connection-socket conn))
656
657 (let loop ((line (read-line s))
658 (result '()))
659 (cond ((eof-object? line) (reverse result))
660 ((regexp-exec %ftp-ready-rx line)
661 =>
662 (lambda (match)
663 (let ((code (string->number (match:substring match 1))))
664 (if (= 126 code)
665 (reverse result)
666 (throw 'ftp-error conn "LIST" code)))))
667 (else
668 (loop (read-line s)
669 (match (reverse (string-tokenize line))
670 ((file _ ... permissions)
671 (let ((type (case (string-ref permissions 0)
672 ((#\d) 'directory)
673 (else 'file))))
674 (cons (list file type) result)))
675 ((file _ ...)
676 (cons (cons file 'file) result))))))))
677 (lambda ()
678 (close s)
679 (let-values (((code message) (%ftp-listen (ftp-connection-socket conn))))
680 (or (eqv? code 226)
681 (throw 'ftp-error conn "LIST" code message)))))))
682
683
684;;;
685;;; GNU.
686;;;
687
688(define %ignored-package-attributes
689 ;; Attribute name of packages to be ignored.
690 '("bash" "bashReal" "bashInteractive" ;; the full versioned name is incorrect
691 "autoconf213"
692 "automake17x"
693 "automake19x"
694 "automake110x"
695 "bison1875"
696 "bison23"
697 "bison24"
698 "bison" ;; = 2.4
699 "ccrtp_1_8"
700 "emacs22"
701 "emacsSnapshot"
702 "gcc295"
703 "gcc33"
704 "gcc34"
705 "gcc40"
706 "gcc41"
707 "gcc42"
708 "gcc43"
709 "gcc44"
710 "gcc45"
711 "gcc45_real"
712 "gcc45_realCross"
713 "gfortran45"
714 "gcj45"
715 "gcc46"
716 "gcc46_real"
717 "gcc46_realCross"
718 "gfortran46"
719 "gcj46"
720 "glibc25"
721 "glibc27"
722 "glibc29"
723 "guile_1_8"
724 "icecat3"
725 "icecat3Xul" ;; redundant with `icecat'
726 "icecatWrapper"
727 "icecat3Wrapper"
728 "icecatXulrunner3"
729 "libzrtpcpp_1_6"
730 "parted_2_3"
731 ))
732
733(define (gnu? package)
734 ;; Return true if PACKAGE (a snix expression) is a GNU package (according
735 ;; to a simple heuristic.) Otherwise return #f.
736 (match package
737 (('attribute _ _ ('derivation _ _ body))
738 (any (lambda (attr)
739 (match attr
740 (('attribute _ "meta" ('attribute-set metas))
741 (any (lambda (attr)
742 (match attr
743 (('attribute _ "description" value)
744 (string-prefix? "GNU" value))
745 (('attribute _ "homepage" (? string? value))
746 (or (string-contains value "gnu.org")
747 (string-contains value "gnupg.org")))
748 (('attribute _ "homepage" ((? string? value) ...))
749 (any (cut string-contains <> "www.gnu.org") value))
750 (_ #f)))
751 metas))
752 (_ #f)))
753 body))
754 (_ #f)))
755
756(define (gnu-packages packages)
757 (fold (lambda (package gnu)
758 (match package
759 (('attribute _ "emacs23Packages" emacs-packages)
760 ;; XXX: Should prepend `emacs23Packages.' to attribute names.
761 (append (gnu-packages emacs-packages) gnu))
762 (('attribute _ attribute-name ('derivation _ _ body))
763 (if (member attribute-name %ignored-package-attributes)
764 gnu
765 (if (gnu? package)
766 (cons package gnu)
767 gnu)))
768 (_ gnu)))
769 '()
770 packages))
771
772(define (ftp-server/directory project)
773 (define quirks
774 '(("commoncpp2" "ftp.gnu.org" "/gnu/commoncpp" #f)
775 ("ucommon" "ftp.gnu.org" "/gnu/commoncpp" #f)
776 ("libzrtpcpp" "ftp.gnu.org" "/gnu/ccrtp" #f)
777 ("libosip2" "ftp.gnu.org" "/gnu/osip" #f)
778 ("libgcrypt" "ftp.gnupg.org" "/gcrypt" #t)
779 ("libgpg-error" "ftp.gnupg.org" "/gcrypt" #t)
780 ("libassuan" "ftp.gnupg.org" "/gcrypt" #t)
781 ("freefont-ttf" "ftp.gnu.org" "/gnu/freefont" #f)
782 ("gnupg" "ftp.gnupg.org" "/gcrypt" #t)
783 ("gnu-ghostscript" "ftp.gnu.org" "/gnu/ghostscript" #f)
784 ("mit-scheme" "ftp.gnu.org" "/gnu/mit-scheme/stable.pkg" #f)
785 ("icecat" "ftp.gnu.org" "/gnu/gnuzilla" #f)
786 ("source-highlight" "ftp.gnu.org" "/gnu/src-highlite" #f)
787 ("TeXmacs" "ftp.texmacs.org" "/TeXmacs/targz" #f)))
788
789 (let ((quirk (assoc project quirks)))
790 (match quirk
791 ((_ server directory subdir?)
792 (values server (if (not subdir?)
793 directory
794 (string-append directory "/" project))))
795 (_
796 (values "ftp.gnu.org" (string-append "/gnu/" project))))))
797
798(define (nixpkgs->gnu-name project)
799 (define quirks
800 '(("gcc-wrapper" . "gcc")
801 ("ghostscript" . "gnu-ghostscript") ;; ../ghostscript/gnu-ghoscript-X.Y.tar.gz
802 ("gnum4" . "m4")
803 ("gnugrep" . "grep")
804 ("gnumake" . "make")
805 ("gnused" . "sed")
806 ("gnutar" . "tar")
807 ("mitscheme" . "mit-scheme")
808 ("texmacs" . "TeXmacs")))
809
810 (or (assoc-ref quirks project) project))
811
812(define (releases project)
813 "Return the list of releases of PROJECT as a list of release name/directory
814pairs. Example: (\"mit-scheme-9.0.1\" . \"/gnu/mit-scheme/stable.pkg/9.0.1\"). "
815 ;; TODO: Parse something like fencepost.gnu.org:/gd/gnuorg/packages-ftp.
816 (define release-rx
817 (make-regexp (string-append "^" project
818 "-([0-9]|[^-])*(-src)?\\.tar\\.")))
819
820 (define alpha-rx
821 (make-regexp "^.*-.*[0-9](-|~)?(alpha|beta|rc|cvs|svn|git)-?[0-9\\.]*\\.tar\\."))
822
823 (define (sans-extension tarball)
824 (let ((end (string-contains tarball ".tar")))
825 (substring tarball 0 end)))
826
827 (catch 'ftp-error
828 (lambda ()
829 (let-values (((server directory) (ftp-server/directory project)))
830 (define conn (ftp-open server))
831
832 (let loop ((directories (list directory))
833 (result '()))
834 (if (null? directories)
835 (begin
836 (ftp-close conn)
837 result)
838 (let* ((directory (car directories))
839 (files (ftp-list conn directory))
840 (subdirs (filter-map (lambda (file)
841 (match file
842 ((name 'directory . _) name)
843 (_ #f)))
844 files)))
845 (loop (append (map (cut string-append directory "/" <>)
846 subdirs)
847 (cdr directories))
848 (append
849 ;; Filter out signatures, deltas, and files which are potentially
850 ;; not releases of PROJECT (e.g., in /gnu/guile, filter out
851 ;; guile-oops and guile-www; in mit-scheme, filter out
852 ;; binaries).
853 (filter-map (lambda (file)
854 (match file
855 ((file 'file . _)
856 (and (not (string-suffix? ".sig" file))
857 (regexp-exec release-rx file)
858 (not (regexp-exec alpha-rx file))
859 (let ((s (sans-extension file)))
860 (and (regexp-exec
861 %package-name-rx s)
862 (cons s directory)))))
863 (_ #f)))
864 files)
865 result)))))))
866 (lambda (key subr message . args)
867 (format (current-error-port)
868 "failed to get release list for `~A': ~S ~S~%"
869 project message args)
870 '())))
871
872(define version-string>?
873 (let ((strverscmp
874 (let ((sym (or (dynamic-func "strverscmp" (dynamic-link))
875 (error "could not find `strverscmp' (from GNU libc)"))))
876 (pointer->procedure int sym (list '* '*)))))
877 (lambda (a b)
878 (> (strverscmp (string->pointer a) (string->pointer b)) 0))))
879
880(define (latest-release project)
881 "Return (\"FOO-X.Y\" . \"/bar/foo\") or #f."
882 (let ((releases (releases project)))
883 (and (not (null? releases))
884 (fold (lambda (release latest)
885 (if (version-string>? (car release) (car latest))
886 release
887 latest))
888 '("" . "")
889 releases))))
890
891(define %package-name-rx
892 ;; Regexp for a package name, e.g., "foo-X.Y". Since TeXmacs uses
893 ;; "TeXmacs-X.Y-src", the `-src' suffix is allowed.
894 (make-regexp "^(.*)-(([0-9]|\\.)+)(-src)?"))
895
896(define (package/version name+version)
897 "Return the package name and version number extracted from NAME+VERSION."
898 (let ((match (regexp-exec %package-name-rx name+version)))
899 (if (not match)
900 (values name+version #f)
901 (values (match:substring match 1) (match:substring match 2)))))
902
903(define (file-extension file)
904 (let ((dot (string-rindex file #\.)))
905 (and dot (substring file (+ 1 dot) (string-length file)))))
906
907(define (packages-to-update gnu-packages)
908 (define (unpack latest)
909 (call-with-values (lambda ()
910 (package/version (car latest)))
911 (lambda (name version)
912 (list name version (cdr latest)))))
913
914 (fold (lambda (pkg result)
915 (call-with-package pkg
916 (lambda (attribute name+version location meta src)
917 (let-values (((name old-version)
918 (package/version name+version)))
919 (let ((latest (latest-release (nixpkgs->gnu-name name))))
920 (if (not latest)
921 (begin
922 (format #t "~A [unknown latest version]~%"
923 name+version)
924 result)
925 (match (unpack latest)
926 ((_ (? (cut string=? old-version <>)) _)
927 (format #t "~A [up to date]~%" name+version)
928 result)
929 ((project new-version directory)
930 (let-values (((old-name old-hash old-urls)
931 (src->values src)))
932 (format #t "~A -> ~A [~A]~%"
933 name+version (car latest)
934 (and (pair? old-urls) (car old-urls)))
935 (let* ((url (and (pair? old-urls)
936 (car old-urls)))
937 (new-hash (fetch-gnu project directory
938 new-version
939 (if url
940 (file-extension url)
941 "gz"))))
942 (cons (list name attribute
943 old-version old-hash
944 new-version new-hash
945 location)
946 result)))))))))))
947 '()
948 gnu-packages))
949
950(define (fetch-gnu project directory version archive-type)
951 "Download PROJECT's tarball over FTP."
952 (let* ((server (ftp-server/directory project))
953 (base (string-append project "-" version ".tar." archive-type))
954 (url (string-append "ftp://" server "/" directory "/" base))
955 (sig (string-append base ".sig"))
956 (sig-url (string-append url ".sig")))
957 (let-values (((hash path) (nix-prefetch-url url)))
958 (pk 'prefetch-url url hash path)
959 (and hash path
960 (begin
961 (false-if-exception (delete-file sig))
962 (system* "wget" sig-url)
963 (if (file-exists? sig)
964 (let ((ret (gnupg-verify* sig path)))
965 (false-if-exception (delete-file sig))
966 (if ret
967 hash
968 (begin
969 (format (current-error-port)
970 "signature verification failed for `~a'~%"
971 base)
972 (format (current-error-port)
973 "(could be because the public key is not in your keyring)~%")
974 #f)))
975 (begin
976 (format (current-error-port)
977 "no signature for `~a'~%" base)
978 hash)))))))
979
980
981;;;
982;;; Main program.
983;;;
984
985(define %options
986 ;; Specifications of the command-line options.
987 (list (option '(#\h "help") #f #f
988 (lambda (opt name arg result)
989 (format #t "Usage: gnupdate [OPTIONS...]~%")
990 (format #t "GNUpdate -- update Nix expressions of GNU packages in Nixpkgs~%")
991 (format #t "~%")
992 (format #t " -x, --xml=FILE Read XML output of `nix-instantiate'~%")
993 (format #t " from FILE.~%")
994 (format #t " -A, --attribute=ATTR~%")
995 (format #t " Update only the package pointed to by attribute~%")
996 (format #t " ATTR.~%")
997 (format #t " -s, --select=SET Update only packages from SET, which may~%")
998 (format #t " be either `all', `stdenv', or `non-stdenv'.~%")
999 (format #t " -d, --dry-run Don't actually update Nix expressions~%")
1000 (format #t " -h, --help Give this help list.~%~%")
1001 (format #t "Report bugs to <ludo@gnu.org>~%")
1002 (exit 0)))
1003 (option '(#\A "attribute") #t #f
1004 (lambda (opt name arg result)
1005 (alist-cons 'attribute arg result)))
1006 (option '(#\s "select") #t #f
1007 (lambda (opt name arg result)
1008 (cond ((string-ci=? arg "stdenv")
1009 (alist-cons 'filter 'stdenv result))
1010 ((string-ci=? arg "non-stdenv")
1011 (alist-cons 'filter 'non-stdenv result))
1012 ((string-ci=? arg "all")
1013 (alist-cons 'filter #f result))
1014 (else
1015 (format (current-error-port)
1016 "~A: unrecognized selection type~%"
1017 arg)
1018 (exit 1)))))
1019
1020 (option '(#\d "dry-run") #f #f
1021 (lambda (opt name arg result)
1022 (alist-cons 'dry-run #t result)))
1023
1024 (option '(#\x "xml") #t #f
1025 (lambda (opt name arg result)
1026 (alist-cons 'xml-file arg result)))))
1027
1028(define (gnupdate . args)
1029 ;; Assume Nixpkgs is under $NIXPKGS or ~/src/nixpkgs.
1030
1031 (define (nixpkgs->snix xml-file attribute)
1032 (format (current-error-port) "evaluating Nixpkgs...~%")
1033 (let* ((home (getenv "HOME"))
1034 (xml (if xml-file
1035 (open-input-file xml-file)
1036 (open-nixpkgs (or (getenv "NIXPKGS")
1037 (string-append home "/src/nixpkgs"))
1038 attribute)))
1039 (snix (xml->snix xml)))
1040 (if (not xml-file)
1041 (let ((status (pipe-failed? xml)))
1042 (if status
1043 (begin
1044 (format (current-error-port) "`nix-instantiate' failed: ~A~%"
1045 status)
1046 (exit 1)))))
1047
1048 ;; If we asked for a specific attribute, rewrap the thing in an
1049 ;; attribute set to match the expectations of `packages-to-update' & co.
1050 (if attribute
1051 (match snix
1052 (('snix loc ('derivation args ...))
1053 `(snix ,loc
1054 (attribute-set
1055 ((attribute #f ,attribute
1056 (derivation ,@args)))))))
1057 snix)))
1058
1059 (define (selected-gnu-packages packages stdenv selection)
1060 ;; Return the subset of PACKAGES that are/aren't in STDENV, according to
1061 ;; SELECTION. To do that reliably, we check whether their "src"
1062 ;; derivation is a requisite of STDENV.
1063 (define gnu
1064 (gnu-packages packages))
1065
1066 (case selection
1067 ((stdenv)
1068 (filter (lambda (p)
1069 (member (package-source-output-path p)
1070 (force stdenv)))
1071 gnu))
1072 ((non-stdenv)
1073 (filter (lambda (p)
1074 (not (member (package-source-output-path p)
1075 (force stdenv))))
1076 gnu))
1077 (else gnu)))
1078
1079 (let* ((opts (args-fold (cdr args) %options
1080 (lambda (opt name arg result)
1081 (error "unrecognized option `~A'" name))
1082 (lambda (operand result)
1083 (error "extraneous argument `~A'" operand))
1084 '()))
1085 (snix (nixpkgs->snix (assq-ref opts 'xml-file)
1086 (assq-ref opts 'attribute)))
1087 (packages (match snix
1088 (('snix _ ('attribute-set attributes))
1089 attributes)
1090 (_ #f)))
1091 (stdenv (delay
1092 ;; The source tarballs that make up stdenv.
1093 (filter-map derivation-source-output-path
1094 (package-requisites (stdenv-package packages)))))
1095 (attribute (assq-ref opts 'attribute))
1096 (selection (assq-ref opts 'filter))
1097 (to-update (if attribute
1098 packages ; already a subset
1099 (selected-gnu-packages packages stdenv selection)))
1100 (updates (packages-to-update to-update)))
1101
1102 (format #t "~%~A packages to update...~%" (length updates))
1103 (for-each (lambda (update)
1104 (match update
1105 ((name attribute
1106 old-version old-hash
1107 new-version new-hash
1108 location)
1109 (if (assoc-ref opts 'dry-run)
1110 (format #t "`~a' would be updated from ~a to ~a (~a -> ~a)~%"
1111 name old-version new-version
1112 old-hash new-hash)
1113 (update-nix-expression (location-file location)
1114 old-version old-hash
1115 new-version new-hash)))
1116 (_ #f)))
1117 updates)
1118 #t))
1119
1120;;; Local Variables:
1121;;; eval: (put 'call-with-package 'scheme-indent-function 1)
1122;;; End: