Commit | Line | Data |
---|---|---|
0a7c5a09 | 1 | ;;; GNU Guix --- Functional package management for GNU |
3b2dc9ed | 2 | ;;; Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org> |
7e6b490d | 3 | ;;; Copyright © 2015 Alex Kost <alezost@gmail.com> |
0a7c5a09 LC |
4 | ;;; |
5 | ;;; This file is part of GNU Guix. | |
6 | ;;; | |
7 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | |
8 | ;;; under the terms of the GNU General Public License as published by | |
9 | ;;; the Free Software Foundation; either version 3 of the License, or (at | |
10 | ;;; your option) any later version. | |
11 | ;;; | |
12 | ;;; GNU Guix is distributed in the hope that it will be useful, but | |
13 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | ;;; GNU General Public License for more details. | |
16 | ;;; | |
17 | ;;; You should have received a copy of the GNU General Public License | |
18 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | |
19 | ||
20 | (define-module (guix upstream) | |
21 | #:use-module (guix records) | |
22 | #:use-module (guix utils) | |
23 | #:use-module ((guix download) | |
24 | #:select (download-to-store)) | |
0a7c5a09 LC |
25 | #:use-module (guix gnupg) |
26 | #:use-module (guix packages) | |
27 | #:use-module (guix ui) | |
28 | #:use-module (guix base32) | |
8d5d0628 LC |
29 | #:use-module (guix gexp) |
30 | #:use-module (guix store) | |
31 | #:use-module ((guix derivations) | |
32 | #:select (built-derivations derivation->output-path)) | |
33 | #:use-module (guix monads) | |
0a7c5a09 LC |
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 (ice-9 match) | |
39 | #:use-module (ice-9 regex) | |
40 | #:export (upstream-source | |
41 | upstream-source? | |
42 | upstream-source-package | |
43 | upstream-source-version | |
44 | upstream-source-urls | |
45 | upstream-source-signature-urls | |
3195e19d | 46 | upstream-source-archive-types |
0a7c5a09 | 47 | |
97abc907 | 48 | url-prefix-predicate |
0a7c5a09 LC |
49 | coalesce-sources |
50 | ||
51 | upstream-updater | |
52 | upstream-updater? | |
53 | upstream-updater-name | |
7e6b490d | 54 | upstream-updater-description |
0a7c5a09 LC |
55 | upstream-updater-predicate |
56 | upstream-updater-latest | |
57 | ||
e9c72306 LC |
58 | lookup-updater |
59 | ||
0a7c5a09 | 60 | download-tarball |
e9c72306 LC |
61 | package-latest-release |
62 | package-latest-release* | |
0a7c5a09 LC |
63 | package-update |
64 | update-package-source)) | |
65 | ||
66 | ;;; Commentary: | |
67 | ;;; | |
68 | ;;; This module provides tools to represent and manipulate a upstream source | |
69 | ;;; code, and to auto-update package recipes. | |
70 | ;;; | |
71 | ;;; Code: | |
72 | ||
73 | ;; Representation of upstream's source. There can be several URLs--e.g., | |
74 | ;; tar.gz, tar.gz, etc. There can be correspond signature URLs, one per | |
75 | ;; source URL. | |
76 | (define-record-type* <upstream-source> | |
77 | upstream-source make-upstream-source | |
78 | upstream-source? | |
79 | (package upstream-source-package) ;string | |
80 | (version upstream-source-version) ;string | |
81 | (urls upstream-source-urls) ;list of strings | |
82 | (signature-urls upstream-source-signature-urls ;#f | list of strings | |
83 | (default #f))) | |
84 | ||
97abc907 LC |
85 | (define (url-prefix-predicate prefix) |
86 | "Return a predicate that returns true when passed a package where one of its | |
87 | source URLs starts with PREFIX." | |
88 | (lambda (package) | |
89 | (define matching-uri? | |
90 | (match-lambda | |
91 | ((? string? uri) | |
92 | (string-prefix? prefix uri)) | |
93 | (_ | |
94 | #f))) | |
95 | ||
96 | (match (package-source package) | |
97 | ((? origin? origin) | |
98 | (match (origin-uri origin) | |
99 | ((? matching-uri?) #t) | |
100 | (_ #f))) | |
101 | (_ #f)))) | |
102 | ||
0a7c5a09 LC |
103 | (define (upstream-source-archive-types release) |
104 | "Return the available types of archives for RELEASE---a list of strings such | |
105 | as \"gz\" or \"xz\"." | |
106 | (map file-extension (upstream-source-urls release))) | |
107 | ||
108 | (define (coalesce-sources sources) | |
109 | "Coalesce the elements of SOURCES, a list of <upstream-source>, that | |
110 | correspond to the same version." | |
111 | (define (same-version? r1 r2) | |
112 | (string=? (upstream-source-version r1) (upstream-source-version r2))) | |
113 | ||
114 | (define (release>? r1 r2) | |
115 | (version>? (upstream-source-version r1) (upstream-source-version r2))) | |
116 | ||
117 | (fold (lambda (release result) | |
118 | (match result | |
119 | ((head . tail) | |
120 | (if (same-version? release head) | |
121 | (cons (upstream-source | |
122 | (inherit release) | |
123 | (urls (append (upstream-source-urls release) | |
124 | (upstream-source-urls head))) | |
125 | (signature-urls | |
6efa6f76 | 126 | (let ((one (upstream-source-signature-urls release)) |
f1eacbaf | 127 | (two (upstream-source-signature-urls head))) |
6efa6f76 | 128 | (and one two (append one two))))) |
0a7c5a09 LC |
129 | tail) |
130 | (cons release result))) | |
131 | (() | |
132 | (list release)))) | |
133 | '() | |
134 | (sort sources release>?))) | |
135 | ||
136 | \f | |
137 | ;;; | |
138 | ;;; Auto-update. | |
139 | ;;; | |
140 | ||
7e6b490d AK |
141 | (define-record-type* <upstream-updater> |
142 | upstream-updater make-upstream-updater | |
0a7c5a09 | 143 | upstream-updater? |
7e6b490d AK |
144 | (name upstream-updater-name) |
145 | (description upstream-updater-description) | |
146 | (pred upstream-updater-predicate) | |
147 | (latest upstream-updater-latest)) | |
0a7c5a09 LC |
148 | |
149 | (define (lookup-updater package updaters) | |
150 | "Return an updater among UPDATERS that matches PACKAGE, or #f if none of | |
151 | them matches." | |
152 | (any (match-lambda | |
3b2dc9ed | 153 | (($ <upstream-updater> name description pred latest) |
0a7c5a09 LC |
154 | (and (pred package) latest))) |
155 | updaters)) | |
156 | ||
e9c72306 | 157 | (define (package-latest-release package updaters) |
7d27a025 | 158 | "Return an upstream source to update PACKAGE, a <package> object, or #f if |
e9c72306 LC |
159 | none of UPDATERS matches PACKAGE. It is the caller's responsibility to ensure |
160 | that the returned source is newer than the current one." | |
0a7c5a09 LC |
161 | (match (lookup-updater package updaters) |
162 | ((? procedure? latest-release) | |
e9c72306 LC |
163 | (latest-release package)) |
164 | (_ #f))) | |
165 | ||
166 | (define (package-latest-release* package updaters) | |
167 | "Like 'package-latest-release', but ensure that the return source is newer | |
168 | than that of PACKAGE." | |
169 | (match (package-latest-release package updaters) | |
170 | ((and source ($ <upstream-source> name version)) | |
171 | (and (version>? version (package-version package)) | |
172 | source)) | |
173 | (_ | |
174 | #f))) | |
0a7c5a09 | 175 | |
8d5d0628 LC |
176 | (define (uncompressed-tarball name tarball) |
177 | "Return a derivation that decompresses TARBALL." | |
178 | (define (ref package) | |
179 | (module-ref (resolve-interface '(gnu packages compression)) | |
180 | package)) | |
181 | ||
182 | (define compressor | |
183 | (cond ((or (string-suffix? ".gz" tarball) | |
184 | (string-suffix? ".tgz" tarball)) | |
185 | (file-append (ref 'gzip) "/bin/gzip")) | |
186 | ((string-suffix? ".bz2" tarball) | |
187 | (file-append (ref 'bzip2) "/bin/bzip2")) | |
188 | ((string-suffix? ".xz" tarball) | |
189 | (file-append (ref 'xz) "/bin/xz")) | |
190 | ((string-suffix? ".lz" tarball) | |
191 | (file-append (ref 'lzip) "/bin/lzip")) | |
192 | (else | |
193 | (error "unknown archive type" tarball)))) | |
194 | ||
195 | (gexp->derivation (file-sans-extension name) | |
196 | #~(begin | |
197 | (copy-file #+tarball #+name) | |
198 | (and (zero? (system* #+compressor "-d" #+name)) | |
199 | (copy-file #+(file-sans-extension name) | |
200 | #$output))))) | |
201 | ||
0a7c5a09 LC |
202 | (define* (download-tarball store url signature-url |
203 | #:key (key-download 'interactive)) | |
204 | "Download the tarball at URL to the store; check its OpenPGP signature at | |
205 | SIGNATURE-URL, unless SIGNATURE-URL is false. On success, return the tarball | |
14959038 LC |
206 | file name; return #f on failure (network failure or authentication failure). |
207 | KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed | |
208 | values: 'interactive' (default), 'always', and 'never'." | |
0a7c5a09 LC |
209 | (let ((tarball (download-to-store store url))) |
210 | (if (not signature-url) | |
211 | tarball | |
8d5d0628 LC |
212 | (let* ((sig (download-to-store store signature-url)) |
213 | ||
214 | ;; Sometimes we get a signature over the uncompressed tarball. | |
215 | ;; In that case, decompress the tarball in the store so that we | |
216 | ;; can check the signature. | |
217 | (data (if (string-prefix? (basename url) | |
218 | (basename signature-url)) | |
219 | tarball | |
220 | (run-with-store store | |
221 | (mlet %store-monad ((drv (uncompressed-tarball | |
222 | (basename url) tarball))) | |
223 | (mbegin %store-monad | |
224 | (built-derivations (list drv)) | |
225 | (return (derivation->output-path drv))))))) | |
226 | ||
227 | (ret (gnupg-verify* sig data #:key-download key-download))) | |
0a7c5a09 LC |
228 | (if ret |
229 | tarball | |
230 | (begin | |
69daee23 | 231 | (warning (G_ "signature verification failed for `~a'~%") |
0a7c5a09 | 232 | url) |
69daee23 | 233 | (warning (G_ "(could be because the public key is not in your keyring)~%")) |
0a7c5a09 LC |
234 | #f)))))) |
235 | ||
236 | (define (find2 pred lst1 lst2) | |
237 | "Like 'find', but operate on items from both LST1 and LST2. Return two | |
238 | values: the item from LST1 and the item from LST2 that match PRED." | |
239 | (let loop ((lst1 lst1) (lst2 lst2)) | |
240 | (match lst1 | |
241 | ((head1 . tail1) | |
242 | (match lst2 | |
243 | ((head2 . tail2) | |
244 | (if (pred head1 head2) | |
245 | (values head1 head2) | |
246 | (loop tail1 tail2))))) | |
247 | (() | |
248 | (values #f #f))))) | |
249 | ||
250 | (define* (package-update store package updaters | |
251 | #:key (key-download 'interactive)) | |
252 | "Return the new version and the file name of the new version tarball for | |
253 | PACKAGE, or #f and #f when PACKAGE is up-to-date. KEY-DOWNLOAD specifies a | |
254 | download policy for missing OpenPGP keys; allowed values: 'always', 'never', | |
255 | and 'interactive' (default)." | |
e9c72306 | 256 | (match (package-latest-release* package updaters) |
0a7c5a09 LC |
257 | (($ <upstream-source> _ version urls signature-urls) |
258 | (let*-values (((name) | |
259 | (package-name package)) | |
260 | ((archive-type) | |
261 | (match (and=> (package-source package) origin-uri) | |
262 | ((? string? uri) | |
6976c681 | 263 | (file-extension (basename uri))) |
0a7c5a09 LC |
264 | (_ |
265 | "gz"))) | |
266 | ((url signature-url) | |
267 | (find2 (lambda (url sig-url) | |
af72a21a LC |
268 | ;; Some URIs lack a file extension, like |
269 | ;; 'https://crates.io/???/0.1/download'. In that | |
270 | ;; case, pick the first URL. | |
271 | (or (not archive-type) | |
272 | (string-suffix? archive-type url))) | |
0a7c5a09 LC |
273 | urls |
274 | (or signature-urls (circular-list #f))))) | |
275 | (let ((tarball (download-tarball store url signature-url | |
276 | #:key-download key-download))) | |
277 | (values version tarball)))) | |
278 | (#f | |
279 | (values #f #f)))) | |
280 | ||
281 | (define (update-package-source package version hash) | |
282 | "Modify the source file that defines PACKAGE to refer to VERSION, | |
283 | whose tarball has SHA256 HASH (a bytevector). Return the new version string | |
284 | if an update was made, and #f otherwise." | |
2b8e9d9e SB |
285 | (define (update-expression expr old-version version old-hash hash) |
286 | ;; Update package expression EXPR, replacing occurrences OLD-VERSION by | |
287 | ;; VERSION and occurrences of OLD-HASH by HASH (base32 representation | |
288 | ;; thereof). | |
0a7c5a09 LC |
289 | (let ((old-hash (bytevector->nix-base32-string old-hash)) |
290 | (hash (bytevector->nix-base32-string hash))) | |
2b8e9d9e SB |
291 | (string-replace-substring |
292 | (string-replace-substring expr old-hash hash) | |
293 | old-version version))) | |
294 | ||
295 | (let ((name (package-name package)) | |
296 | (version-loc (package-field-location package 'version))) | |
297 | (if version-loc | |
298 | (let* ((loc (package-location package)) | |
299 | (old-version (package-version package)) | |
300 | (old-hash (origin-sha256 (package-source package))) | |
301 | (file (and=> (location-file loc) | |
302 | (cut search-path %load-path <>)))) | |
0a7c5a09 | 303 | (if file |
2b8e9d9e SB |
304 | (and (edit-expression |
305 | ;; Be sure to use absolute filename. | |
306 | (assq-set! (location->source-properties loc) | |
307 | 'filename file) | |
308 | (cut update-expression <> | |
309 | old-version version old-hash hash)) | |
310 | version) | |
0a7c5a09 | 311 | (begin |
69daee23 | 312 | (warning (G_ "~a: could not locate source file") |
0a7c5a09 LC |
313 | (location-file loc)) |
314 | #f))) | |
315 | (begin | |
316 | (format (current-error-port) | |
69daee23 | 317 | (G_ "~a: ~a: no `version' field in source; skipping~%") |
0a7c5a09 LC |
318 | (location->string (package-location package)) |
319 | name))))) | |
320 | ||
321 | ;;; upstream.scm ends here |