git-authenticate: Factorize 'authenticate-repository'.
[jackhill/guix/guix.git] / guix / channels.scm
CommitLineData
0d39a3b9 1;;; GNU Guix --- Functional package management for GNU
f75243e1 2;;; Copyright © 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
af12790b 3;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net>
53f21642 4;;; Copyright © 2019 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
0d39a3b9
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 channels)
8ba7fd3c 22 #:use-module (git)
0d39a3b9 23 #:use-module (guix git)
43badf26
LC
24 #:use-module (guix git-authenticate)
25 #:use-module ((guix openpgp)
26 #:select (openpgp-public-key-fingerprint
27 openpgp-format-fingerprint))
28 #:use-module (guix base16)
0d39a3b9
LC
29 #:use-module (guix records)
30 #:use-module (guix gexp)
5fbdc9a5 31 #:use-module (guix modules)
0d39a3b9
LC
32 #:use-module (guix discovery)
33 #:use-module (guix monads)
34 #:use-module (guix profiles)
37c0d458 35 #:use-module (guix packages)
43badf26 36 #:use-module (guix progress)
0d39a3b9 37 #:use-module (guix derivations)
f58f676b 38 #:use-module (guix combinators)
69962ab7 39 #:use-module (guix diagnostics)
8ba7fd3c 40 #:use-module (guix sets)
0d39a3b9
LC
41 #:use-module (guix store)
42 #:use-module (guix i18n)
ab6025b5
LC
43 #:use-module ((guix utils)
44 #:select (source-properties->location
f75243e1
LC
45 &error-location
46 &fix-hint))
0d39a3b9 47 #:use-module (srfi srfi-1)
af12790b 48 #:use-module (srfi srfi-2)
0d39a3b9
LC
49 #:use-module (srfi srfi-9)
50 #:use-module (srfi srfi-11)
9719e8d3 51 #:use-module (srfi srfi-26)
ab6025b5
LC
52 #:use-module (srfi srfi-34)
53 #:use-module (srfi srfi-35)
5fbdc9a5
LC
54 #:autoload (guix self) (whole-package make-config.scm)
55 #:autoload (guix inferior) (gexp->derivation-in-inferior) ;FIXME: circular dep
20507347 56 #:autoload (guix quirks) (%quirks %patches applicable-patch? apply-patch)
43badf26 57 #:use-module (ice-9 format)
0d39a3b9 58 #:use-module (ice-9 match)
ed75bdf3 59 #:use-module (ice-9 vlist)
37c0d458 60 #:use-module ((ice-9 rdelim) #:select (read-string))
43badf26 61 #:use-module ((rnrs bytevectors) #:select (bytevector=?))
0d39a3b9
LC
62 #:export (channel
63 channel?
64 channel-name
65 channel-url
66 channel-branch
67 channel-commit
43badf26 68 channel-introduction
0d39a3b9
LC
69 channel-location
70
43badf26 71 channel-introduction?
8b7d982e
LC
72 make-channel-introduction
73 channel-introduction-first-signed-commit
74 channel-introduction-first-commit-signer
43badf26 75
6577682a
LC
76 openpgp-fingerprint->bytevector
77 openpgp-fingerprint
78
0d39a3b9 79 %default-channels
72f749dc 80 guix-channel?
0d39a3b9
LC
81
82 channel-instance?
83 channel-instance-channel
84 channel-instance-commit
85 channel-instance-checkout
86
43badf26 87 authenticate-channel
0d39a3b9 88 latest-channel-instances
fe5db4eb 89 checkout->channel-instance
030f1367 90 latest-channel-derivation
c37f38bd 91 channel-instances->manifest
5fbdc9a5 92 %channel-profile-hooks
a7c714d3 93 channel-instances->derivation
872898f7 94 ensure-forward-channel-update
a7c714d3 95
8ba7fd3c
LC
96 profile-channels
97
98 channel-news-entry?
99 channel-news-entry-commit
9719e8d3 100 channel-news-entry-tag
8ba7fd3c
LC
101 channel-news-entry-title
102 channel-news-entry-body
103
104 channel-news-for-commit))
0d39a3b9
LC
105
106;;; Commentary:
107;;;
108;;; This module implements "channels." A channel is usually a source of
109;;; package definitions. There's a special channel, the 'guix' channel, that
110;;; provides all of Guix, including its commands and its documentation.
111;;; User-defined channels are expected to typically provide a bunch of .scm
112;;; files meant to be added to the '%package-search-path'.
113;;;
114;;; This module provides tools to fetch and update channels from a Git
115;;; repository and to build them.
116;;;
117;;; Code:
118
119(define-record-type* <channel> channel make-channel
120 channel?
121 (name channel-name)
122 (url channel-url)
123 (branch channel-branch (default "master"))
124 (commit channel-commit (default #f))
43badf26 125 (introduction channel-introduction (default #f))
0d39a3b9
LC
126 (location channel-location
127 (default (current-source-location)) (innate)))
0d39a3b9 128
43badf26
LC
129;; Channel introductions. A "channel introduction" provides a commit/signer
130;; pair that specifies the first commit of the authentication process as well
22a96992
LC
131;; as its signer's fingerprint. Introductions are used to bootstrap trust in
132;; a channel.
43badf26 133(define-record-type <channel-introduction>
22a96992 134 (%make-channel-introduction first-signed-commit first-commit-signer)
43badf26 135 channel-introduction?
22a96992
LC
136 (first-signed-commit channel-introduction-first-signed-commit) ;hex string
137 (first-commit-signer channel-introduction-first-commit-signer)) ;bytevector
43badf26 138
8b7d982e
LC
139(define (make-channel-introduction commit signer)
140 "Return a new channel introduction: COMMIT is the introductory where
141authentication starts, and SIGNER is the OpenPGP fingerprint (a bytevector) of
142the signer of that commit."
22a96992 143 (%make-channel-introduction commit signer))
8b7d982e 144
6577682a
LC
145(define (openpgp-fingerprint->bytevector str)
146 "Convert STR, an OpenPGP fingerprint (hexadecimal string with whitespace),
147to the corresponding bytevector."
148 (base16-string->bytevector
149 (string-downcase (string-filter char-set:hex-digit str))))
150
151(define-syntax openpgp-fingerprint
152 (lambda (s)
153 "Convert STR, an OpenPGP fingerprint (hexadecimal string with whitespace),
154to the corresponding bytevector."
155 (syntax-case s ()
156 ((_ str)
157 (string? (syntax->datum #'str))
158 (openpgp-fingerprint->bytevector (syntax->datum #'str)))
159 ((_ str)
160 #'(openpgp-fingerprint->bytevector str)))))
161
43badf26
LC
162(define %guix-channel-introduction
163 ;; Introduction of the official 'guix channel. The chosen commit is the
164 ;; first one that introduces '.guix-authorizations' on the 'staging'
165 ;; branch that was eventually merged in 'master'. Any branch starting
166 ;; before that commit cannot be merged or it will be rejected by 'guix pull'
167 ;; & co.
168 (make-channel-introduction
169 "9edb3f66fd807b096b48283debdcddccfea34bad" ;2020-05-26
6577682a
LC
170 (openpgp-fingerprint ;mbakke
171 "BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA")))
43badf26 172
c3f6f564
LC
173(define %default-channel-url
174 ;; URL of the default 'guix' channel.
175 "https://git.savannah.gnu.org/git/guix.git")
176
0d39a3b9
LC
177(define %default-channels
178 ;; Default list of channels.
179 (list (channel
180 (name 'guix)
37a6cdbf 181 (branch "master")
c3f6f564 182 (url %default-channel-url)
43badf26 183 (introduction %guix-channel-introduction))))
0d39a3b9
LC
184
185(define (guix-channel? channel)
186 "Return true if CHANNEL is the 'guix' channel."
187 (eq? 'guix (channel-name channel)))
188
c3f6f564
LC
189(define (ensure-default-introduction chan)
190 "If CHAN represents the \"official\" 'guix' channel and lacks an
191introduction, add it."
192 (if (and (guix-channel? chan)
193 (not (channel-introduction chan))
194 (string=? (channel-url chan) %default-channel-url))
195 (channel (inherit chan)
196 (introduction %guix-channel-introduction))
197 chan))
198
0d39a3b9
LC
199(define-record-type <channel-instance>
200 (channel-instance channel commit checkout)
201 channel-instance?
202 (channel channel-instance-channel)
203 (commit channel-instance-commit)
204 (checkout channel-instance-checkout))
205
af12790b 206(define-record-type <channel-metadata>
4ae762af 207 (channel-metadata directory dependencies news-file keyring-reference url)
af12790b 208 channel-metadata?
ce5d9ec8 209 (directory channel-metadata-directory) ;string with leading slash
8ba7fd3c 210 (dependencies channel-metadata-dependencies) ;list of <channel>
43badf26 211 (news-file channel-metadata-news-file) ;string | #f
4ae762af
LC
212 (keyring-reference channel-metadata-keyring-reference) ;string
213 (url channel-metadata-url)) ;string | #f
43badf26
LC
214
215(define %default-keyring-reference
216 ;; Default value of the 'keyring-reference' field.
217 "keyring")
af12790b 218
0d39a3b9
LC
219(define (channel-reference channel)
220 "Return the \"reference\" for CHANNEL, an sexp suitable for
221'latest-repository-commit'."
222 (match (channel-commit channel)
223 (#f `(branch . ,(channel-branch channel)))
224 (commit `(commit . ,(channel-commit channel)))))
225
d774c7b1
LC
226(define sexp->channel-introduction
227 (match-lambda
228 (('channel-introduction ('version 0)
229 ('commit commit) ('signer signer)
230 _ ...)
231 (make-channel-introduction commit (openpgp-fingerprint signer)))
232 (x #f)))
233
45b90332
LC
234(define (read-channel-metadata port)
235 "Read from PORT channel metadata in the format expected for the
236'.guix-channel' file. Return a <channel-metadata> record, or raise an error
237if valid metadata could not be read from PORT."
238 (match (read port)
239 (('channel ('version 0) properties ...)
240 (let ((directory (and=> (assoc-ref properties 'directory) first))
8ba7fd3c 241 (dependencies (or (assoc-ref properties 'dependencies) '()))
43badf26 242 (news-file (and=> (assoc-ref properties 'news-file) first))
4ae762af 243 (url (and=> (assoc-ref properties 'url) first))
43badf26
LC
244 (keyring-reference
245 (or (and=> (assoc-ref properties 'keyring-reference) first)
246 %default-keyring-reference)))
45b90332 247 (channel-metadata
8ba7fd3c 248 (cond ((not directory) "/") ;directory
ce5d9ec8
LC
249 ((string-prefix? "/" directory) directory)
250 (else (string-append "/" directory)))
8ba7fd3c 251 (map (lambda (item) ;dependencies
45b90332
LC
252 (let ((get (lambda* (key #:optional default)
253 (or (and=> (assoc-ref item key) first) default))))
254 (and-let* ((name (get 'name))
255 (url (get 'url))
256 (branch (get 'branch "master")))
257 (channel
258 (name name)
259 (branch branch)
260 (url url)
d774c7b1
LC
261 (commit (get 'commit))
262 (introduction (and=> (get 'introduction)
263 sexp->channel-introduction))))))
8ba7fd3c 264 dependencies)
43badf26 265 news-file
4ae762af
LC
266 keyring-reference
267 url)))
45b90332
LC
268 ((and ('channel ('version version) _ ...) sexp)
269 (raise (condition
270 (&message (message "unsupported '.guix-channel' version"))
271 (&error-location
272 (location (source-properties->location
273 (source-properties sexp)))))))
274 (sexp
275 (raise (condition
276 (&message (message "invalid '.guix-channel' file"))
277 (&error-location
278 (location (source-properties->location
279 (source-properties sexp)))))))))
280
53f21642
JN
281(define (read-channel-metadata-from-source source)
282 "Return a channel-metadata record read from channel's SOURCE/.guix-channel
ce5d9ec8
LC
283description file, or return the default channel-metadata record if that file
284doesn't exist."
45b90332
LC
285 (catch 'system-error
286 (lambda ()
287 (call-with-input-file (string-append source "/.guix-channel")
288 read-channel-metadata))
289 (lambda args
290 (if (= ENOENT (system-error-errno args))
4ae762af 291 (channel-metadata "/" '() #f %default-keyring-reference #f)
45b90332
LC
292 (apply throw args)))))
293
294(define (channel-instance-metadata instance)
53f21642 295 "Return a channel-metadata record read from the channel INSTANCE's
ce5d9ec8 296description file or its default value."
53f21642
JN
297 (read-channel-metadata-from-source (channel-instance-checkout instance)))
298
af12790b
RW
299(define (channel-instance-dependencies instance)
300 "Return the list of channels that are declared as dependencies for the given
301channel INSTANCE."
ce5d9ec8 302 (channel-metadata-dependencies (channel-instance-metadata instance)))
af12790b 303
053b10c3
LC
304(define (apply-patches checkout commit patches)
305 "Apply the matching PATCHES to CHECKOUT, modifying files in place. The
306result is unspecified."
307 (let loop ((patches patches))
308 (match patches
309 (() #t)
20507347
LC
310 ((patch rest ...)
311 (when (applicable-patch? patch checkout commit)
312 (apply-patch patch checkout))
053b10c3
LC
313 (loop rest)))))
314
43badf26
LC
315(define commit-short-id
316 (compose (cut string-take <> 7) oid->string commit-id))
317
43badf26
LC
318(define* (authenticate-channel channel checkout commit
319 #:key (keyring-reference-prefix "origin/"))
320 "Authenticate the given COMMIT of CHANNEL, available at CHECKOUT, a
321directory containing a CHANNEL checkout. Raise an error if authentication
322fails."
838f2bdf
LC
323 (define intro
324 (channel-introduction channel))
325
326 (define cache-key
327 (string-append "channels/" (symbol->string (channel-name channel))))
328
329 (define keyring-reference
330 (channel-metadata-keyring-reference
331 (read-channel-metadata-from-source checkout)))
332
333 (define (make-reporter start-commit end-commit commits)
334 (format (current-error-port)
335 (G_ "Authenticating channel '~a', commits ~a to ~a (~h new \
336commits)...~%")
337 (channel-name channel)
338 (commit-short-id start-commit)
339 (commit-short-id end-commit)
340 (length commits))
341
342 (progress-reporter/bar (length commits)))
343
43badf26
LC
344 ;; XXX: Too bad we need to re-open CHECKOUT.
345 (with-repository checkout repository
838f2bdf
LC
346 (authenticate-repository repository
347 (string->oid
348 (channel-introduction-first-signed-commit intro))
349 (channel-introduction-first-commit-signer intro)
350 #:end (string->oid commit)
351 #:keyring-reference
352 (string-append keyring-reference-prefix
353 keyring-reference)
354 #:make-reporter make-reporter
355 #:cache-key cache-key)))
43badf26 356
053b10c3 357(define* (latest-channel-instance store channel
872898f7 358 #:key (patches %patches)
5bafc70d 359 starting-commit
a9eeeaa6 360 (authenticate? #f)
5bafc70d
LC
361 (validate-pull
362 ensure-forward-channel-update))
363 "Return the latest channel instance for CHANNEL. When STARTING-COMMIT is
364true, call VALIDATE-PULL with CHANNEL, STARTING-COMMIT, the target commit, and
a9eeeaa6 365their relation. When AUTHENTICATE? is false, CHANNEL is not authenticated."
053b10c3
LC
366 (define (dot-git? file stat)
367 (and (string=? (basename file) ".git")
368 (eq? 'directory (stat:type stat))))
369
c3f6f564
LC
370 (let-values (((channel)
371 (ensure-default-introduction channel))
372 ((checkout commit relation)
053b10c3 373 (update-cached-checkout (channel-url channel)
872898f7
LC
374 #:ref (channel-reference channel)
375 #:starting-commit starting-commit)))
5bafc70d
LC
376 (when relation
377 (validate-pull channel starting-commit commit relation))
378
a9eeeaa6
LC
379 (if authenticate?
380 (if (channel-introduction channel)
381 (authenticate-channel channel checkout commit)
382 ;; TODO: Warn for all the channels once the authentication interface
383 ;; is public.
384 (when (guix-channel? channel)
ead5c461
LC
385 (raise (condition
386 (&message
387 (message (format #f (G_ "channel '~a' lacks an \
388introduction and cannot be authenticated~%")
389 (channel-name channel))))
390 (&fix-hint
391 (hint (G_ "Add the missing introduction to your
392channels file to address the issue. Alternatively, you can pass
393@option{--disable-authentication}, at the risk of running unauthenticated and
394thus potentially malicious code.")))))))
a9eeeaa6 395 (warning (G_ "channel authentication disabled~%")))
43badf26 396
053b10c3
LC
397 (when (guix-channel? channel)
398 ;; Apply the relevant subset of PATCHES directly in CHECKOUT. This is
399 ;; safe to do because 'switch-to-ref' eventually does a hard reset.
400 (apply-patches checkout commit patches))
401
402 (let* ((name (url+commit->name (channel-url channel) commit))
403 (checkout (add-to-store store name #t "sha256" checkout
404 #:select? (negate dot-git?))))
5bafc70d 405 (channel-instance channel commit checkout))))
872898f7 406
5bafc70d 407(define (ensure-forward-channel-update channel start commit relation)
872898f7 408 "Raise an error if RELATION is not 'ancestor, meaning that START is not an
5bafc70d 409ancestor of COMMIT, unless CHANNEL specifies a commit.
872898f7
LC
410
411This procedure implements a channel update policy meant to be used as a
412#:validate-pull argument."
413 (match relation
414 ('ancestor #t)
415 ('self #t)
416 (_
9744cc7b
LC
417 (raise (make-compound-condition
418 (condition
419 (&message (message
420 (format #f (G_ "\
872898f7 421aborting update of channel '~a' to commit ~a, which is not a descendant of ~a")
9744cc7b 422 (channel-name channel)
5bafc70d 423 commit start))))
872898f7 424
9744cc7b
LC
425 ;; If the user asked for a specific commit, they might want
426 ;; that to happen nevertheless, so tell them about the
427 ;; relevant 'guix pull' option.
428 (if (channel-commit channel)
429 (condition
430 (&fix-hint
431 (hint (G_ "Use @option{--allow-downgrades} to force
432this downgrade."))))
433 (condition
434 (&fix-hint
435 (hint (G_ "This could indicate that the channel has
872898f7
LC
436been tampered with and is trying to force a roll-back, preventing you from
437getting the latest updates. If you think this is not the case, explicitly
9744cc7b 438allow non-forward updates."))))))))))
872898f7 439
4ae762af
LC
440(define (channel-instance-primary-url instance)
441 "Return the primary URL advertised for INSTANCE, or #f if there is no such
442information."
443 (channel-metadata-url (channel-instance-metadata instance)))
444
872898f7
LC
445(define* (latest-channel-instances store channels
446 #:key
447 (current-channels '())
a9eeeaa6 448 (authenticate? #t)
872898f7
LC
449 (validate-pull
450 ensure-forward-channel-update))
0d39a3b9 451 "Return a list of channel instances corresponding to the latest checkouts of
872898f7
LC
452CHANNELS and the channels on which they depend.
453
a9eeeaa6
LC
454When AUTHENTICATE? is true, authenticate the subset of CHANNELS that has a
455\"channel introduction\".
456
872898f7
LC
457CURRENT-CHANNELS is the list of currently used channels. It is compared
458against the newly-fetched instances of CHANNELS, and VALIDATE-PULL is called
459for each channel update and can choose to emit warnings or raise an error,
460depending on the policy it implements."
af12790b
RW
461 ;; Only process channels that are unique, or that are more specific than a
462 ;; previous channel specification.
463 (define (ignore? channel others)
464 (member channel others
465 (lambda (a b)
466 (and (eq? (channel-name a) (channel-name b))
467 (or (channel-commit b)
468 (not (or (channel-commit a)
469 (channel-commit b))))))))
f58f676b 470
872898f7
LC
471 (define (current-commit name)
472 ;; Return the current commit for channel NAME.
473 (any (lambda (channel)
474 (and (eq? (channel-name channel) name)
475 (channel-commit channel)))
476 current-channels))
477
9b049de8
LC
478 (let loop ((channels channels)
479 (previous-channels '()))
480 ;; Accumulate a list of instances. A list of processed channels is also
481 ;; accumulated to decide on duplicate channel specifications.
482 (define-values (resulting-channels instances)
483 (fold2 (lambda (channel previous-channels instances)
484 (if (ignore? channel previous-channels)
485 (values previous-channels instances)
486 (begin
487 (format (current-error-port)
488 (G_ "Updating channel '~a' from Git repository at '~a'...~%")
489 (channel-name channel)
490 (channel-url channel))
5bafc70d
LC
491 (let* ((current (current-commit (channel-name channel)))
492 (instance
493 (latest-channel-instance store channel
a9eeeaa6
LC
494 #:authenticate?
495 authenticate?
5bafc70d
LC
496 #:validate-pull
497 validate-pull
498 #:starting-commit
499 current)))
4ae762af
LC
500 (when authenticate?
501 ;; CHANNEL is authenticated so we can trust the
502 ;; primary URL advertised in its metadata and warn
503 ;; about possibly stale mirrors.
504 (let ((primary-url (channel-instance-primary-url
505 instance)))
506 (unless (or (not primary-url)
507 (channel-commit channel)
508 (string=? primary-url (channel-url channel)))
509 (warning (G_ "pulled channel '~a' from a mirror \
510of ~a, which might be stale~%")
511 (channel-name channel)
512 primary-url))))
872898f7 513
9b049de8
LC
514 (let-values (((new-instances new-channels)
515 (loop (channel-instance-dependencies instance)
516 previous-channels)))
517 (values (append (cons channel new-channels)
518 previous-channels)
519 (append (cons instance new-instances)
520 instances)))))))
521 previous-channels
522 '() ;instances
523 channels))
f58f676b 524
9b049de8
LC
525 (let ((instance-name (compose channel-name channel-instance-channel)))
526 ;; Remove all earlier channel specifications if they are followed by a
527 ;; more specific one.
528 (values (delete-duplicates instances
529 (lambda (a b)
530 (eq? (instance-name a) (instance-name b))))
531 resulting-channels))))
0d39a3b9 532
fe5db4eb
LC
533(define* (checkout->channel-instance checkout
534 #:key commit
535 (url checkout) (name 'guix))
536 "Return a channel instance for CHECKOUT, which is assumed to be a checkout
537of COMMIT at URL. Use NAME as the channel name."
538 (let* ((commit (or commit (make-string 40 #\0)))
539 (channel (channel (name name)
540 (commit commit)
541 (url url))))
542 (channel-instance channel commit checkout)))
543
0d39a3b9
LC
544(define %self-build-file
545 ;; The file containing code to build Guix. This serves the same purpose as
546 ;; a makefile, and, similarly, is intended to always keep this name.
547 "build-aux/build-self.scm")
548
549(define %pull-version
550 ;; This is the version of the 'guix pull' protocol. It specifies what's
551 ;; expected from %SELF-BUILD-FILE. The initial version ("0") was when we'd
552 ;; place a set of compiled Guile modules in ~/.config/guix/latest.
553 1)
554
acefa740
LC
555(define (standard-module-derivation name source core dependencies)
556 "Return a derivation that builds with CORE, a Guix instance, the Scheme
557modules in SOURCE and that depend on DEPENDENCIES, a list of lowerable
558objects. The assumption is that SOURCE contains package modules to be added
559to '%package-module-path'."
acefa740 560
53f21642 561 (let* ((metadata (read-channel-metadata-from-source source))
ce5d9ec8 562 (directory (channel-metadata-directory metadata)))
53f21642
JN
563
564 (define build
565 ;; This is code that we'll run in CORE, a Guix instance, with its own
566 ;; modules and so on. That way, we make sure these modules are built for
567 ;; the right Guile version, with the right dependencies, and that they get
568 ;; to see the right (gnu packages …) modules.
569 (with-extensions dependencies
570 #~(begin
571 (use-modules (guix build compile)
572 (guix build utils)
573 (srfi srfi-26))
574
575 (define go
576 (string-append #$output "/lib/guile/" (effective-version)
577 "/site-ccache"))
578 (define scm
579 (string-append #$output "/share/guile/site/"
580 (effective-version)))
acefa740 581
ce5d9ec8 582 (let* ((subdir #$directory)
53f21642
JN
583 (source (string-append #$source subdir)))
584 (compile-files source go (find-files source "\\.scm$"))
585 (mkdir-p (dirname scm))
586 (symlink (string-append #$source subdir) scm))
acefa740 587
53f21642 588 scm)))
acefa740 589
53f21642 590 (gexp->derivation-in-inferior name build core)))
0d39a3b9 591
37c0d458
LC
592(define* (guile-for-source source #:optional (quirks %quirks))
593 "Return the Guile package to use when building SOURCE or #f if the default
594'%guile-for-build' should be good enough."
595 (let loop ((quirks quirks))
596 (match quirks
597 (()
598 #f)
599 (((predicate . guile) rest ...)
600 (if (predicate source) (guile) (loop rest))))))
601
9db88369
LC
602(define (call-with-guile guile thunk)
603 (lambda (store)
604 (values (parameterize ((%guile-for-build
605 (if guile
606 (package-derivation store guile)
607 (%guile-for-build))))
608 (run-with-store store (thunk)))
609 store)))
610
611(define-syntax-rule (with-guile guile exp ...)
612 "Set GUILE as the '%guile-for-build' parameter for the dynamic extent of
613EXP, a series of monadic expressions."
614 (call-with-guile guile (lambda ()
615 (mbegin %store-monad exp ...))))
616
42a87136
LC
617(define (with-trivial-build-handler mvalue)
618 "Run MVALUE, a monadic value, with a \"trivial\" build handler installed
619that unconditionally resumes the continuation."
620 (lambda (store)
621 (with-build-handler (lambda (continue . _)
622 (continue #t))
623 (values (run-with-store store mvalue)
624 store))))
625
0d39a3b9 626(define* (build-from-source name source
acefa740 627 #:key core verbose? commit
0d39a3b9
LC
628 (dependencies '()))
629 "Return a derivation to build Guix from SOURCE, using the self-build script
acefa740
LC
630contained therein; use COMMIT as the version string. When CORE is true, build
631package modules under SOURCE using CORE, an instance of Guix."
0d39a3b9
LC
632 ;; Running the self-build script makes it easier to update the build
633 ;; procedure: the self-build script of the Guix-to-be-installed contains the
634 ;; right dependencies, build procedure, etc., which the Guix-in-use may not
635 ;; be know.
636 (define script
637 (string-append source "/" %self-build-file))
638
639 (if (file-exists? script)
640 (let ((build (save-module-excursion
641 (lambda ()
3a8c4860
LC
642 ;; Disable deprecation warnings; it's OK for SCRIPT to
643 ;; use deprecated APIs and the user doesn't have to know
644 ;; about it.
69962ab7 645 (parameterize ((guix-warning-port
3a8c4860 646 (%make-void-port "w")))
37c0d458
LC
647 (primitive-load script)))))
648 (guile (guile-for-source source)))
0d39a3b9
LC
649 ;; BUILD must be a monadic procedure of at least one argument: the
650 ;; source tree.
651 ;;
652 ;; Note: BUILD can return #f if it does not support %PULL-VERSION. In
653 ;; the future we'll fall back to a previous version of the protocol
654 ;; when that happens.
9db88369 655 (with-guile guile
42a87136
LC
656 ;; BUILD is usually quite costly. Install a "trivial" build handler
657 ;; so we don't bounce an outer build-accumulator handler that could
658 ;; cause us to redo half of the BUILD computation several times just
659 ;; to realize it gives the same result.
660 (with-trivial-build-handler
661 (build source #:verbose? verbose? #:version commit
662 #:pull-version %pull-version))))
0d39a3b9
LC
663
664 ;; Build a set of modules that extend Guix using the standard method.
acefa740 665 (standard-module-derivation name source core dependencies)))
0d39a3b9 666
acefa740
LC
667(define* (build-channel-instance instance
668 #:optional core (dependencies '()))
0d39a3b9
LC
669 "Return, as a monadic value, the derivation for INSTANCE, a channel
670instance. DEPENDENCIES is a list of extensions providing Guile modules that
671INSTANCE depends on."
672 (build-from-source (symbol->string
673 (channel-name (channel-instance-channel instance)))
674 (channel-instance-checkout instance)
675 #:commit (channel-instance-commit instance)
acefa740 676 #:core core
0d39a3b9
LC
677 #:dependencies dependencies))
678
ed75bdf3
LC
679(define (resolve-dependencies instances)
680 "Return a procedure that, given one of the elements of INSTANCES, returns
681list of instances it depends on."
682 (define channel-instance-name
683 (compose channel-name channel-instance-channel))
684
685 (define table ;map a name to an instance
686 (fold (lambda (instance table)
687 (vhash-consq (channel-instance-name instance)
688 instance table))
689 vlist-null
690 instances))
691
692 (define edges
693 (fold (lambda (instance edges)
694 (fold (lambda (channel edges)
695 (let ((name (channel-name channel)))
696 (match (vhash-assq name table)
697 ((_ . target)
698 (vhash-consq instance target edges)))))
699 edges
700 (channel-instance-dependencies instance)))
701 vlist-null
702 instances))
703
704 (lambda (instance)
705 (vhash-foldq* cons '() instance edges)))
706
0d39a3b9
LC
707(define (channel-instance-derivations instances)
708 "Return the list of derivations to build INSTANCES, in the same order as
709INSTANCES."
710 (define core-instance
711 ;; The 'guix' channel is treated specially: it's an implicit dependency of
712 ;; all the other channels.
713 (find (lambda (instance)
714 (guix-channel? (channel-instance-channel instance)))
715 instances))
716
ed75bdf3
LC
717 (define edges
718 (resolve-dependencies instances))
719
720 (define (instance->derivation instance)
cdf68947
LC
721 (mlet %store-monad ((system (current-system)))
722 (mcached (if (eq? instance core-instance)
723 (build-channel-instance instance)
724 (mlet %store-monad ((core (instance->derivation core-instance))
725 (deps (mapm %store-monad instance->derivation
726 (edges instance))))
727 (build-channel-instance instance core deps)))
728 instance
729 system)))
ed75bdf3 730
ab6025b5
LC
731 (unless core-instance
732 (let ((loc (and=> (any (compose channel-location channel-instance-channel)
733 instances)
734 source-properties->location)))
735 (raise (apply make-compound-condition
736 (condition
737 (&message (message "'guix' channel is lacking")))
f75243e1
LC
738 (condition
739 (&fix-hint (hint (G_ "Make sure your list of channels
740contains one channel named @code{guix} providing the core of Guix."))))
ab6025b5
LC
741 (if loc
742 (list (condition (&error-location (location loc))))
743 '())))))
744
ed75bdf3 745 (mapm %store-monad instance->derivation instances))
0d39a3b9 746
0d39a3b9
LC
747(define (whole-package-for-legacy name modules)
748 "Return a full-blown Guix package for MODULES, a derivation that builds Guix
749modules in the old ~/.config/guix/latest style."
750 (define packages
751 (resolve-interface '(gnu packages guile)))
752
49c35bbb
LC
753 (define modules+compiled
754 ;; Since MODULES contains both .scm and .go files at its root, re-bundle
755 ;; it so that it has share/guile/site and lib/guile, which is what
756 ;; 'whole-package' expects.
757 (computed-file (derivation-name modules)
758 (with-imported-modules '((guix build utils))
759 #~(begin
760 (use-modules (guix build utils))
761
762 (define version
763 (effective-version))
764 (define share
765 (string-append #$output "/share/guile/site"))
766 (define lib
767 (string-append #$output "/lib/guile/" version))
768
769 (mkdir-p share) (mkdir-p lib)
770 (symlink #$modules (string-append share "/" version))
771 (symlink #$modules (string-append lib "/site-ccache"))))))
772
0d39a3b9
LC
773 (letrec-syntax ((list (syntax-rules (->)
774 ((_)
775 '())
776 ((_ (module -> variable) rest ...)
777 (cons (module-ref (resolve-interface
778 '(gnu packages module))
779 'variable)
780 (list rest ...)))
781 ((_ variable rest ...)
782 (cons (module-ref packages 'variable)
783 (list rest ...))))))
49c35bbb 784 (whole-package name modules+compiled
0d39a3b9
LC
785
786 ;; In the "old style", %SELF-BUILD-FILE would simply return a
787 ;; derivation that builds modules. We have to infer what the
788 ;; dependencies of these modules were.
b74ed909 789 (list guile-json-3 guile-git guile-bytestructures
0d39a3b9
LC
790 (ssh -> guile-ssh) (tls -> gnutls)))))
791
792(define (old-style-guix? drv)
793 "Return true if DRV corresponds to a ~/.config/guix/latest style of
794derivation."
795 ;; Here we rely on a gross historical fact: that derivations produced by the
796 ;; "old style" (before commit 8a0d9bc8a3f153159d9e239a151c0fa98f1e12d8,
797 ;; dated May 30, 2018) did not depend on "guix-command.drv".
798 (not (find (lambda (input)
9af75a26
LC
799 (string=? "guix-command"
800 (derivation-name
801 (derivation-input-derivation input))))
0d39a3b9
LC
802 (derivation-inputs drv))))
803
804(define (channel-instances->manifest instances)
805 "Return a profile manifest with entries for all of INSTANCES, a list of
806channel instances."
d9e6217f 807 (define (instance->entry instance drv)
471550c2
LC
808 (let* ((commit (channel-instance-commit instance))
809 (channel (channel-instance-channel instance))
810 (intro (channel-introduction channel)))
d9e6217f
LC
811 (manifest-entry
812 (name (symbol->string (channel-name channel)))
813 (version (string-take commit 7))
814 (item (if (guix-channel? channel)
815 (if (old-style-guix? drv)
816 (whole-package-for-legacy (string-append name "-" version)
817 drv)
818 drv)
819 drv))
820 (properties
821 `((source (repository
822 (version 0)
823 (url ,(channel-url channel))
824 (branch ,(channel-branch channel))
471550c2
LC
825 (commit ,commit)
826 ,@(if intro
827 `((introduction
828 (channel-introduction
829 (version 0)
830 (commit
831 ,(channel-introduction-first-signed-commit
832 intro))
833 (signer
834 ,(openpgp-format-fingerprint
835 (channel-introduction-first-commit-signer
836 intro))))))
837 '()))))))))
0d39a3b9
LC
838
839 (mlet* %store-monad ((derivations (channel-instance-derivations instances))
d9e6217f 840 (entries -> (map instance->entry instances derivations)))
0d39a3b9 841 (return (manifest entries))))
030f1367 842
5fbdc9a5
LC
843(define (package-cache-file manifest)
844 "Build a package cache file for the instance in MANIFEST. This is meant to
845be used as a profile hook."
ccbc427f 846 (let ((profile (profile (content manifest) (hooks '()))))
5fbdc9a5
LC
847 (define build
848 #~(begin
849 (use-modules (gnu packages))
850
851 (if (defined? 'generate-package-cache)
852 (begin
853 ;; Delegate package cache generation to the inferior.
854 (format (current-error-port)
855 "Generating package cache for '~a'...~%"
856 #$profile)
857 (generate-package-cache #$output))
858 (mkdir #$output))))
859
860 (gexp->derivation-in-inferior "guix-package-cache" build
861 profile
4035fcba
LC
862
863 ;; If the Guix in PROFILE is too old and
864 ;; lacks 'guix repl', don't build the cache
865 ;; instead of failing.
866 #:silent-failure? #t
867
5fbdc9a5 868 #:properties '((type . profile-hook)
f674bc66
LC
869 (hook . package-cache))
870 #:local-build? #t)))
5fbdc9a5
LC
871
872(define %channel-profile-hooks
873 ;; The default channel profile hooks.
874 (cons package-cache-file %default-profile-hooks))
875
c37f38bd
LC
876(define (channel-instances->derivation instances)
877 "Return the derivation of the profile containing INSTANCES, a list of
878channel instances."
879 (mlet %store-monad ((manifest (channel-instances->manifest instances)))
5fbdc9a5
LC
880 (profile-derivation manifest
881 #:hooks %channel-profile-hooks)))
c37f38bd 882
030f1367
LC
883(define latest-channel-instances*
884 (store-lift latest-channel-instances))
885
872898f7
LC
886(define* (latest-channel-derivation #:optional (channels %default-channels)
887 #:key
888 (current-channels '())
889 (validate-pull
890 ensure-forward-channel-update))
030f1367 891 "Return as a monadic value the derivation that builds the profile for the
872898f7
LC
892latest instances of CHANNELS. CURRENT-CHANNELS and VALIDATE-PULL are passed
893to 'latest-channel-instances'."
894 (mlet %store-monad ((instances
895 (latest-channel-instances* channels
896 #:current-channels
897 current-channels
898 #:validate-pull
899 validate-pull)))
c37f38bd 900 (channel-instances->derivation instances)))
a7c714d3
LC
901
902(define (profile-channels profile)
903 "Return the list of channels corresponding to entries in PROFILE. If
904PROFILE is not a profile created by 'guix pull', return the empty list."
905 (filter-map (lambda (entry)
906 (match (assq 'source (manifest-entry-properties entry))
907 (('source ('repository ('version 0)
908 ('url url)
909 ('branch branch)
910 ('commit commit)
471550c2 911 rest ...))
a7c714d3
LC
912 (channel (name (string->symbol
913 (manifest-entry-name entry)))
914 (url url)
471550c2
LC
915 (commit commit)
916 (introduction
917 (match (assq 'introduction rest)
918 (#f #f)
919 (('introduction intro)
920 (sexp->channel-introduction intro))))))
a7c714d3
LC
921
922 ;; No channel information for this manifest entry.
923 ;; XXX: Pre-0.15.0 Guix did not provide that information,
924 ;; but there's not much we can do in that case.
925 (_ #f)))
926
927 ;; Show most recently installed packages last.
928 (reverse
929 (manifest-entries (profile-manifest profile)))))
8ba7fd3c
LC
930
931\f
932;;;
933;;; News.
934;;;
935
936;; Channel news.
937(define-record-type <channel-news>
938 (channel-news entries)
939 channel-news?
940 (entries channel-news-entries)) ;list of <channel-news-entry>
941
942;; News entry, associated with a specific commit of the channel.
943(define-record-type <channel-news-entry>
9719e8d3 944 (channel-news-entry commit tag title body)
8ba7fd3c 945 channel-news-entry?
9719e8d3
LC
946 (commit channel-news-entry-commit) ;hex string | #f
947 (tag channel-news-entry-tag) ;#f | string
8ba7fd3c
LC
948 (title channel-news-entry-title) ;list of language tag/string pairs
949 (body channel-news-entry-body)) ;list of language tag/string pairs
950
951(define (sexp->channel-news-entry entry)
952 "Return the <channel-news-entry> record corresponding to ENTRY, an sexp."
953 (define (pair language message)
954 (cons (symbol->string language) message))
955
956 (match entry
9719e8d3 957 (('entry ((and (or 'commit 'tag) type) commit-or-tag)
8ba7fd3c
LC
958 ('title ((? symbol? title-tags) (? string? titles)) ...)
959 ('body ((? symbol? body-tags) (? string? bodies)) ...)
960 _ ...)
9719e8d3
LC
961 (channel-news-entry (and (eq? type 'commit) commit-or-tag)
962 (and (eq? type 'tag) commit-or-tag)
8ba7fd3c
LC
963 (map pair title-tags titles)
964 (map pair body-tags bodies)))
965 (_
966 (raise (condition
967 (&message (message "invalid channel news entry"))
968 (&error-location
969 (location (source-properties->location
970 (source-properties entry)))))))))
971
972(define (read-channel-news port)
973 "Read a channel news feed from PORT and return it as a <channel-news>
974record."
975 (match (false-if-exception (read port))
976 (('channel-news ('version 0) entries ...)
977 (channel-news (map sexp->channel-news-entry entries)))
978 (('channel-news ('version version) _ ...)
979 ;; This is an unsupported version from the future. There's nothing wrong
980 ;; with that (the user may simply need to upgrade the 'guix' channel to
981 ;; be able to read it), so silently ignore it.
982 (channel-news '()))
983 (#f
984 (raise (condition
985 (&message (message "syntactically invalid channel news file")))))
986 (sexp
987 (raise (condition
988 (&message (message "invalid channel news file"))
989 (&error-location
990 (location (source-properties->location
991 (source-properties sexp)))))))))
992
9719e8d3
LC
993(define (resolve-channel-news-entry-tag repository entry)
994 "If ENTRY has its 'commit' field set, return ENTRY. Otherwise, lookup
995ENTRY's 'tag' in REPOSITORY and return ENTRY with its 'commit' field set to
996the field its 'tag' refers to. A 'git-error' exception is raised if the tag
997cannot be found."
998 (if (channel-news-entry-commit entry)
999 entry
1000 (let* ((tag (channel-news-entry-tag entry))
1001 (reference (string-append "refs/tags/" tag))
1002 (oid (reference-name->oid repository reference)))
1003 (channel-news-entry (oid->string oid) tag
1004 (channel-news-entry-title entry)
1005 (channel-news-entry-body entry)))))
1006
8ba7fd3c
LC
1007(define* (channel-news-for-commit channel new #:optional old)
1008 "Return a list of <channel-news-entry> for CHANNEL between commits OLD and
1009NEW. When OLD is omitted or is #f, return all the news entries of CHANNEL."
1010 (catch 'git-error
1011 (lambda ()
1012 (let* ((checkout (update-cached-checkout (channel-url channel)
1013 #:ref `(commit . ,new)))
1014 (metadata (read-channel-metadata-from-source checkout))
1015 (news-file (channel-metadata-news-file metadata))
1016 (news-file (and news-file
1017 (string-append checkout "/" news-file))))
1018 (if (and news-file (file-exists? news-file))
9719e8d3
LC
1019 (with-repository checkout repository
1020 (let* ((news (call-with-input-file news-file
1021 read-channel-news))
1022 (entries (map (lambda (entry)
1023 (resolve-channel-news-entry-tag repository
1024 entry))
1025 (channel-news-entries news))))
1026 (if old
8ba7fd3c
LC
1027 (let* ((new (commit-lookup repository (string->oid new)))
1028 (old (commit-lookup repository (string->oid old)))
1029 (commits (list->set
1030 (map (compose oid->string commit-id)
1031 (commit-difference new old)))))
1032 (filter (lambda (entry)
1033 (set-contains? commits
1034 (channel-news-entry-commit entry)))
9719e8d3
LC
1035 entries))
1036 entries)))
8ba7fd3c
LC
1037 '())))
1038 (lambda (key error . rest)
1039 ;; If commit NEW or commit OLD cannot be found, then something must be
1040 ;; wrong (for example, the history of CHANNEL was rewritten and these
1041 ;; commits no longer exist upstream), so quietly return the empty list.
1042 (if (= GIT_ENOTFOUND (git-error-code error))
1043 '()
1044 (apply throw key error rest)))))
9db88369
LC
1045
1046;;; Local Variables:
1047;;; eval: (put 'with-guile 'scheme-indent-function 1)
1048;;; End: