Commit | Line | Data |
---|---|---|
0a7c5a09 | 1 | ;;; GNU Guix --- Functional package management for GNU |
069bb95a | 2 | ;;; Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org> |
7e6b490d | 3 | ;;; Copyright © 2015 Alex Kost <alezost@gmail.com> |
7e634c2f | 4 | ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net> |
0a7c5a09 LC |
5 | ;;; |
6 | ;;; This file is part of GNU Guix. | |
7 | ;;; | |
8 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | |
9 | ;;; under the terms of the GNU General Public License as published by | |
10 | ;;; the Free Software Foundation; either version 3 of the License, or (at | |
11 | ;;; your option) any later version. | |
12 | ;;; | |
13 | ;;; GNU Guix is distributed in the hope that it will be useful, but | |
14 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | ;;; GNU General Public License for more details. | |
17 | ;;; | |
18 | ;;; You should have received a copy of the GNU General Public License | |
19 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | |
20 | ||
21 | (define-module (guix upstream) | |
22 | #:use-module (guix records) | |
23 | #:use-module (guix utils) | |
adf0c531 | 24 | #:use-module (guix discovery) |
0a7c5a09 | 25 | #:use-module ((guix download) |
0bd1498f | 26 | #:select (download-to-store url-fetch)) |
0a7c5a09 LC |
27 | #:use-module (guix gnupg) |
28 | #:use-module (guix packages) | |
1c26219f | 29 | #:use-module (guix diagnostics) |
0a7c5a09 LC |
30 | #:use-module (guix ui) |
31 | #:use-module (guix base32) | |
8d5d0628 LC |
32 | #:use-module (guix gexp) |
33 | #:use-module (guix store) | |
9ab817b2 LC |
34 | #:use-module ((guix derivations) #:select (built-derivations derivation->output-path)) |
35 | #:autoload (gcrypt hash) (port-sha256) | |
8d5d0628 | 36 | #:use-module (guix monads) |
0a7c5a09 LC |
37 | #:use-module (srfi srfi-1) |
38 | #:use-module (srfi srfi-9) | |
39 | #:use-module (srfi srfi-11) | |
40 | #:use-module (srfi srfi-26) | |
0bd1498f LC |
41 | #:use-module (srfi srfi-34) |
42 | #:use-module (srfi srfi-35) | |
42314ffa | 43 | #:use-module (rnrs bytevectors) |
0a7c5a09 LC |
44 | #:use-module (ice-9 match) |
45 | #:use-module (ice-9 regex) | |
46 | #:export (upstream-source | |
47 | upstream-source? | |
48 | upstream-source-package | |
49 | upstream-source-version | |
50 | upstream-source-urls | |
51 | upstream-source-signature-urls | |
3195e19d | 52 | upstream-source-archive-types |
7e634c2f | 53 | upstream-source-input-changes |
0a7c5a09 | 54 | |
00290e73 | 55 | url-predicate |
97abc907 | 56 | url-prefix-predicate |
0a7c5a09 LC |
57 | coalesce-sources |
58 | ||
59 | upstream-updater | |
60 | upstream-updater? | |
61 | upstream-updater-name | |
7e6b490d | 62 | upstream-updater-description |
0a7c5a09 LC |
63 | upstream-updater-predicate |
64 | upstream-updater-latest | |
65 | ||
7e634c2f RW |
66 | upstream-input-change? |
67 | upstream-input-change-name | |
68 | upstream-input-change-type | |
69 | upstream-input-change-action | |
70 | changed-inputs | |
71 | ||
adf0c531 | 72 | %updaters |
e9c72306 LC |
73 | lookup-updater |
74 | ||
0a7c5a09 | 75 | download-tarball |
e9c72306 LC |
76 | package-latest-release |
77 | package-latest-release* | |
0a7c5a09 LC |
78 | package-update |
79 | update-package-source)) | |
80 | ||
81 | ;;; Commentary: | |
82 | ;;; | |
83 | ;;; This module provides tools to represent and manipulate a upstream source | |
84 | ;;; code, and to auto-update package recipes. | |
85 | ;;; | |
86 | ;;; Code: | |
87 | ||
88 | ;; Representation of upstream's source. There can be several URLs--e.g., | |
89 | ;; tar.gz, tar.gz, etc. There can be correspond signature URLs, one per | |
90 | ;; source URL. | |
91 | (define-record-type* <upstream-source> | |
92 | upstream-source make-upstream-source | |
93 | upstream-source? | |
94 | (package upstream-source-package) ;string | |
95 | (version upstream-source-version) ;string | |
96 | (urls upstream-source-urls) ;list of strings | |
97 | (signature-urls upstream-source-signature-urls ;#f | list of strings | |
7e634c2f RW |
98 | (default #f)) |
99 | (input-changes upstream-source-input-changes | |
100 | (default '()) (thunked))) | |
101 | ||
102 | ;; Representation of an upstream input change. | |
103 | (define-record-type* <upstream-input-change> | |
104 | upstream-input-change make-upstream-input-change | |
105 | upstream-input-change? | |
106 | (name upstream-input-change-name) ;string | |
107 | (type upstream-input-change-type) ;symbol: regular | native | propagated | |
108 | (action upstream-input-change-action)) ;symbol: add | remove | |
109 | ||
110 | (define (changed-inputs package package-sexp) | |
111 | "Return a list of input changes for PACKAGE based on the newly imported | |
112 | S-expression PACKAGE-SEXP." | |
113 | (match package-sexp | |
114 | ((and expr ('package fields ...)) | |
115 | (let* ((input->name (match-lambda ((name pkg . out) name))) | |
116 | (new-regular | |
117 | (match expr | |
118 | ((path *** ('inputs | |
119 | ('quasiquote ((label ('unquote sym)) ...)))) label) | |
120 | (_ '()))) | |
121 | (new-native | |
122 | (match expr | |
123 | ((path *** ('native-inputs | |
124 | ('quasiquote ((label ('unquote sym)) ...)))) label) | |
125 | (_ '()))) | |
126 | (new-propagated | |
127 | (match expr | |
128 | ((path *** ('propagated-inputs | |
129 | ('quasiquote ((label ('unquote sym)) ...)))) label) | |
130 | (_ '()))) | |
131 | (current-regular | |
132 | (map input->name (package-inputs package))) | |
133 | (current-native | |
134 | (map input->name (package-native-inputs package))) | |
135 | (current-propagated | |
136 | (map input->name (package-propagated-inputs package)))) | |
137 | (append-map | |
138 | (match-lambda | |
139 | ((action type names) | |
140 | (map (lambda (name) | |
141 | (upstream-input-change | |
142 | (name name) | |
143 | (type type) | |
144 | (action action))) | |
145 | names))) | |
146 | `((add regular | |
147 | ,(lset-difference equal? | |
148 | new-regular current-regular)) | |
149 | (remove regular | |
150 | ,(lset-difference equal? | |
151 | current-regular new-regular)) | |
152 | (add native | |
153 | ,(lset-difference equal? | |
154 | new-native current-native)) | |
155 | (remove native | |
156 | ,(lset-difference equal? | |
157 | current-native new-native)) | |
158 | (add propagated | |
159 | ,(lset-difference equal? | |
160 | new-propagated current-propagated)) | |
161 | (remove propagated | |
162 | ,(lset-difference equal? | |
163 | current-propagated new-propagated)))))) | |
164 | (_ '()))) | |
0a7c5a09 | 165 | |
00290e73 LC |
166 | (define* (url-predicate matching-url?) |
167 | "Return a predicate that returns true when passed a package whose source is | |
168 | an <origin> with the URL-FETCH method, and one of its URLs passes | |
169 | MATCHING-URL?." | |
97abc907 | 170 | (lambda (package) |
97abc907 LC |
171 | (match (package-source package) |
172 | ((? origin? origin) | |
00290e73 LC |
173 | (and (eq? (origin-method origin) url-fetch) |
174 | (match (origin-uri origin) | |
175 | ((? string? url) | |
176 | (matching-url? url)) | |
177 | (((? string? urls) ...) | |
178 | (any matching-url? urls)) | |
179 | (_ | |
180 | #f)))) | |
97abc907 LC |
181 | (_ #f)))) |
182 | ||
00290e73 LC |
183 | (define (url-prefix-predicate prefix) |
184 | "Return a predicate that returns true when passed a package where one of its | |
185 | source URLs starts with PREFIX." | |
186 | (url-predicate (cut string-prefix? prefix <>))) | |
187 | ||
0a7c5a09 LC |
188 | (define (upstream-source-archive-types release) |
189 | "Return the available types of archives for RELEASE---a list of strings such | |
190 | as \"gz\" or \"xz\"." | |
191 | (map file-extension (upstream-source-urls release))) | |
192 | ||
193 | (define (coalesce-sources sources) | |
194 | "Coalesce the elements of SOURCES, a list of <upstream-source>, that | |
195 | correspond to the same version." | |
196 | (define (same-version? r1 r2) | |
197 | (string=? (upstream-source-version r1) (upstream-source-version r2))) | |
198 | ||
199 | (define (release>? r1 r2) | |
200 | (version>? (upstream-source-version r1) (upstream-source-version r2))) | |
201 | ||
202 | (fold (lambda (release result) | |
203 | (match result | |
204 | ((head . tail) | |
205 | (if (same-version? release head) | |
206 | (cons (upstream-source | |
207 | (inherit release) | |
208 | (urls (append (upstream-source-urls release) | |
209 | (upstream-source-urls head))) | |
210 | (signature-urls | |
6efa6f76 | 211 | (let ((one (upstream-source-signature-urls release)) |
f1eacbaf | 212 | (two (upstream-source-signature-urls head))) |
6efa6f76 | 213 | (and one two (append one two))))) |
0a7c5a09 LC |
214 | tail) |
215 | (cons release result))) | |
216 | (() | |
217 | (list release)))) | |
218 | '() | |
219 | (sort sources release>?))) | |
220 | ||
221 | \f | |
222 | ;;; | |
223 | ;;; Auto-update. | |
224 | ;;; | |
225 | ||
7e6b490d AK |
226 | (define-record-type* <upstream-updater> |
227 | upstream-updater make-upstream-updater | |
0a7c5a09 | 228 | upstream-updater? |
7e6b490d AK |
229 | (name upstream-updater-name) |
230 | (description upstream-updater-description) | |
231 | (pred upstream-updater-predicate) | |
232 | (latest upstream-updater-latest)) | |
0a7c5a09 | 233 | |
adf0c531 EF |
234 | (define (importer-modules) |
235 | "Return the list of importer modules." | |
236 | (cons (resolve-interface '(guix gnu-maintenance)) | |
237 | (all-modules (map (lambda (entry) | |
238 | `(,entry . "guix/import")) | |
3c0128b0 LC |
239 | %load-path) |
240 | #:warn warn-about-load-error))) | |
adf0c531 EF |
241 | |
242 | (define %updaters | |
243 | ;; The list of publically-known updaters. | |
244 | (delay (fold-module-public-variables (lambda (obj result) | |
245 | (if (upstream-updater? obj) | |
246 | (cons obj result) | |
247 | result)) | |
248 | '() | |
249 | (importer-modules)))) | |
250 | ||
9ab817b2 LC |
251 | ;; Tests need to mock this variable so mark it as "non-declarative". |
252 | (set! %updaters %updaters) | |
253 | ||
069bb95a LC |
254 | (define* (lookup-updater package |
255 | #:optional (updaters (force %updaters))) | |
0a7c5a09 LC |
256 | "Return an updater among UPDATERS that matches PACKAGE, or #f if none of |
257 | them matches." | |
7c101c4c LC |
258 | (find (match-lambda |
259 | (($ <upstream-updater> name description pred latest) | |
260 | (pred package))) | |
261 | updaters)) | |
0a7c5a09 | 262 | |
069bb95a LC |
263 | (define* (package-latest-release package |
264 | #:optional | |
265 | (updaters (force %updaters))) | |
7d27a025 | 266 | "Return an upstream source to update PACKAGE, a <package> object, or #f if |
e9c72306 LC |
267 | none of UPDATERS matches PACKAGE. It is the caller's responsibility to ensure |
268 | that the returned source is newer than the current one." | |
0a7c5a09 | 269 | (match (lookup-updater package updaters) |
7c101c4c LC |
270 | ((? upstream-updater? updater) |
271 | ((upstream-updater-latest updater) package)) | |
e9c72306 LC |
272 | (_ #f))) |
273 | ||
069bb95a LC |
274 | (define* (package-latest-release* package |
275 | #:optional | |
276 | (updaters (force %updaters))) | |
e9c72306 LC |
277 | "Like 'package-latest-release', but ensure that the return source is newer |
278 | than that of PACKAGE." | |
279 | (match (package-latest-release package updaters) | |
280 | ((and source ($ <upstream-source> name version)) | |
281 | (and (version>? version (package-version package)) | |
282 | source)) | |
283 | (_ | |
284 | #f))) | |
0a7c5a09 | 285 | |
8d5d0628 LC |
286 | (define (uncompressed-tarball name tarball) |
287 | "Return a derivation that decompresses TARBALL." | |
288 | (define (ref package) | |
289 | (module-ref (resolve-interface '(gnu packages compression)) | |
290 | package)) | |
291 | ||
292 | (define compressor | |
293 | (cond ((or (string-suffix? ".gz" tarball) | |
294 | (string-suffix? ".tgz" tarball)) | |
295 | (file-append (ref 'gzip) "/bin/gzip")) | |
296 | ((string-suffix? ".bz2" tarball) | |
297 | (file-append (ref 'bzip2) "/bin/bzip2")) | |
298 | ((string-suffix? ".xz" tarball) | |
299 | (file-append (ref 'xz) "/bin/xz")) | |
300 | ((string-suffix? ".lz" tarball) | |
301 | (file-append (ref 'lzip) "/bin/lzip")) | |
302 | (else | |
303 | (error "unknown archive type" tarball)))) | |
304 | ||
305 | (gexp->derivation (file-sans-extension name) | |
306 | #~(begin | |
307 | (copy-file #+tarball #+name) | |
308 | (and (zero? (system* #+compressor "-d" #+name)) | |
309 | (copy-file #+(file-sans-extension name) | |
310 | #$output))))) | |
311 | ||
0a7c5a09 LC |
312 | (define* (download-tarball store url signature-url |
313 | #:key (key-download 'interactive)) | |
314 | "Download the tarball at URL to the store; check its OpenPGP signature at | |
315 | SIGNATURE-URL, unless SIGNATURE-URL is false. On success, return the tarball | |
14959038 LC |
316 | file name; return #f on failure (network failure or authentication failure). |
317 | KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed | |
318 | values: 'interactive' (default), 'always', and 'never'." | |
0a7c5a09 LC |
319 | (let ((tarball (download-to-store store url))) |
320 | (if (not signature-url) | |
321 | tarball | |
8d5d0628 LC |
322 | (let* ((sig (download-to-store store signature-url)) |
323 | ||
324 | ;; Sometimes we get a signature over the uncompressed tarball. | |
325 | ;; In that case, decompress the tarball in the store so that we | |
326 | ;; can check the signature. | |
327 | (data (if (string-prefix? (basename url) | |
328 | (basename signature-url)) | |
329 | tarball | |
330 | (run-with-store store | |
331 | (mlet %store-monad ((drv (uncompressed-tarball | |
332 | (basename url) tarball))) | |
333 | (mbegin %store-monad | |
334 | (built-derivations (list drv)) | |
f94f9d67 LC |
335 | (return (derivation->output-path drv)))))))) |
336 | (let-values (((status data) | |
fa3d9c4d LC |
337 | (if sig |
338 | (gnupg-verify* sig data | |
339 | #:key-download key-download) | |
340 | (values 'missing-signature data)))) | |
f94f9d67 LC |
341 | (match status |
342 | ('valid-signature | |
343 | tarball) | |
fa3d9c4d LC |
344 | ('missing-signature |
345 | (warning (G_ "failed to download detached signature from ~a~%") | |
346 | signature-url) | |
347 | #f) | |
f94f9d67 LC |
348 | ('invalid-signature |
349 | (warning (G_ "signature verification failed for '~a' (key: ~a)~%") | |
350 | url data) | |
351 | #f) | |
352 | ('missing-key | |
353 | (warning (G_ "missing public key ~a for '~a'~%") | |
354 | data url) | |
355 | #f))))))) | |
0a7c5a09 | 356 | |
9ab817b2 LC |
357 | (define-gexp-compiler (upstream-source-compiler (source <upstream-source>) |
358 | system target) | |
359 | "Download SOURCE from its first URL and lower it as a fixed-output | |
360 | derivation that would fetch it." | |
361 | (mlet* %store-monad ((url -> (first (upstream-source-urls source))) | |
362 | (signature | |
363 | -> (and=> (upstream-source-signature-urls source) | |
364 | first)) | |
365 | (tarball ((store-lift download-tarball) url signature))) | |
366 | (unless tarball | |
367 | (raise (formatted-message (G_ "failed to fetch source from '~a'") | |
368 | url))) | |
369 | ||
370 | ;; Instead of returning TARBALL, return a fixed-output derivation that | |
371 | ;; would be able to re-download it. In practice, since TARBALL is already | |
372 | ;; in the store, no extra download will happen, but having the derivation | |
373 | ;; in store improves provenance tracking. | |
374 | (let ((hash (call-with-input-file tarball port-sha256))) | |
375 | (url-fetch url 'sha256 hash (store-path-package-name tarball) | |
376 | #:system system)))) | |
377 | ||
0a7c5a09 LC |
378 | (define (find2 pred lst1 lst2) |
379 | "Like 'find', but operate on items from both LST1 and LST2. Return two | |
380 | values: the item from LST1 and the item from LST2 that match PRED." | |
381 | (let loop ((lst1 lst1) (lst2 lst2)) | |
382 | (match lst1 | |
383 | ((head1 . tail1) | |
384 | (match lst2 | |
385 | ((head2 . tail2) | |
386 | (if (pred head1 head2) | |
387 | (values head1 head2) | |
388 | (loop tail1 tail2))))) | |
389 | (() | |
390 | (values #f #f))))) | |
391 | ||
0bd1498f LC |
392 | (define* (package-update/url-fetch store package source |
393 | #:key key-download) | |
1ee3d2dc | 394 | "Return the version, tarball, and SOURCE, to update PACKAGE to |
0bd1498f LC |
395 | SOURCE, an <upstream-source>." |
396 | (match source | |
1ee3d2dc | 397 | (($ <upstream-source> _ version urls signature-urls) |
0bd1498f | 398 | (let*-values (((archive-type) |
0a7c5a09 LC |
399 | (match (and=> (package-source package) origin-uri) |
400 | ((? string? uri) | |
bc76f8b1 | 401 | (let ((type (or (file-extension (basename uri)) ""))) |
618f0582 LC |
402 | ;; Sometimes we have URLs such as |
403 | ;; "https://github.com/…/tarball/v0.1", in which case | |
404 | ;; we must not consider "1" as the extension. | |
405 | (and (or (string-contains type "z") | |
406 | (string=? type "tar")) | |
407 | type))) | |
0a7c5a09 LC |
408 | (_ |
409 | "gz"))) | |
410 | ((url signature-url) | |
0ea009db | 411 | ;; Try to find a URL that matches ARCHIVE-TYPE. |
0a7c5a09 | 412 | (find2 (lambda (url sig-url) |
af72a21a LC |
413 | ;; Some URIs lack a file extension, like |
414 | ;; 'https://crates.io/???/0.1/download'. In that | |
415 | ;; case, pick the first URL. | |
416 | (or (not archive-type) | |
417 | (string-suffix? archive-type url))) | |
0a7c5a09 LC |
418 | urls |
419 | (or signature-urls (circular-list #f))))) | |
0ea009db LC |
420 | ;; If none of URLS matches ARCHIVE-TYPE, then URL is #f; in that case, |
421 | ;; pick up the first element of URLS. | |
422 | (let ((tarball (download-tarball store | |
423 | (or url (first urls)) | |
424 | (and (pair? signature-urls) | |
425 | (or signature-url | |
426 | (first signature-urls))) | |
0a7c5a09 | 427 | #:key-download key-download))) |
1ee3d2dc | 428 | (values version tarball source)))))) |
0bd1498f LC |
429 | |
430 | (define %method-updates | |
431 | ;; Mapping of origin methods to source update procedures. | |
432 | `((,url-fetch . ,package-update/url-fetch))) | |
433 | ||
069bb95a LC |
434 | (define* (package-update store package |
435 | #:optional (updaters (force %updaters)) | |
0bd1498f LC |
436 | #:key (key-download 'interactive)) |
437 | "Return the new version, the file name of the new version tarball, and input | |
438 | changes for PACKAGE; return #f (three values) when PACKAGE is up-to-date. | |
439 | KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed | |
440 | values: 'always', 'never', and 'interactive' (default)." | |
441 | (match (package-latest-release* package updaters) | |
442 | ((? upstream-source? source) | |
443 | (let ((method (match (package-source package) | |
444 | ((? origin? origin) | |
445 | (origin-method origin)) | |
446 | (_ | |
447 | #f)))) | |
448 | (match (assq method %method-updates) | |
449 | (#f | |
d51bfe24 LC |
450 | (raise (make-compound-condition |
451 | (formatted-message (G_ "cannot download for \ | |
0bd1498f | 452 | this method: ~s") |
d51bfe24 LC |
453 | method) |
454 | (condition | |
455 | (&error-location | |
456 | (location (package-location package))))))) | |
0bd1498f LC |
457 | ((_ . update) |
458 | (update store package source | |
459 | #:key-download key-download))))) | |
0a7c5a09 | 460 | (#f |
7e634c2f | 461 | (values #f #f #f)))) |
0a7c5a09 | 462 | |
42314ffa LC |
463 | (define* (update-package-source package source hash) |
464 | "Modify the source file that defines PACKAGE to refer to SOURCE, an | |
465 | <upstream-source> whose tarball has SHA256 HASH (a bytevector). Return the | |
466 | new version string if an update was made, and #f otherwise." | |
467 | (define (update-expression expr replacements) | |
468 | ;; Apply REPLACEMENTS to package expression EXPR, a string. REPLACEMENTS | |
469 | ;; must be a list of replacement pairs, either bytevectors or strings. | |
470 | (fold (lambda (replacement str) | |
471 | (match replacement | |
472 | (((? bytevector? old-bv) . (? bytevector? new-bv)) | |
473 | (string-replace-substring | |
474 | str | |
475 | (bytevector->nix-base32-string old-bv) | |
476 | (bytevector->nix-base32-string new-bv))) | |
477 | ((old . new) | |
478 | (string-replace-substring str old new)))) | |
479 | expr | |
480 | replacements)) | |
2b8e9d9e SB |
481 | |
482 | (let ((name (package-name package)) | |
42314ffa | 483 | (version (upstream-source-version source)) |
2b8e9d9e SB |
484 | (version-loc (package-field-location package 'version))) |
485 | (if version-loc | |
486 | (let* ((loc (package-location package)) | |
487 | (old-version (package-version package)) | |
c7d2dd69 LC |
488 | (old-hash (content-hash-value |
489 | (origin-hash (package-source package)))) | |
42314ffa LC |
490 | (old-url (match (origin-uri (package-source package)) |
491 | ((? string? url) url) | |
492 | (_ #f))) | |
493 | (new-url (match (upstream-source-urls source) | |
494 | ((first _ ...) first))) | |
2b8e9d9e SB |
495 | (file (and=> (location-file loc) |
496 | (cut search-path %load-path <>)))) | |
0a7c5a09 | 497 | (if file |
42314ffa LC |
498 | ;; Be sure to use absolute filename. Replace the URL directory |
499 | ;; when OLD-URL is available; this is useful notably for | |
500 | ;; mirror://cpan/ URLs where the directory may change as a | |
501 | ;; function of the person who uploads the package. Note that | |
502 | ;; package definitions usually concatenate fragments of the URL, | |
503 | ;; which is why we only attempt to replace a subset of the URL. | |
504 | (let ((properties (assq-set! (location->source-properties loc) | |
505 | 'filename file)) | |
506 | (replacements `((,old-version . ,version) | |
507 | (,old-hash . ,hash) | |
508 | ,@(if (and old-url new-url) | |
509 | `((,(dirname old-url) . | |
510 | ,(dirname new-url))) | |
511 | '())))) | |
512 | (and (edit-expression properties | |
513 | (cut update-expression <> replacements)) | |
514 | version)) | |
0a7c5a09 | 515 | (begin |
69daee23 | 516 | (warning (G_ "~a: could not locate source file") |
0a7c5a09 LC |
517 | (location-file loc)) |
518 | #f))) | |
1c26219f LC |
519 | (warning (package-location package) |
520 | (G_ "~a: no `version' field in source; skipping~%") | |
521 | name)))) | |
0a7c5a09 LC |
522 | |
523 | ;;; upstream.scm ends here |