channels: 'latest-channel-instance' authenticates Git checkouts.
[jackhill/guix/guix.git] / tests / channels.scm
CommitLineData
af12790b
RW
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (test-channels)
20 #:use-module (guix channels)
ed75bdf3 21 #:use-module (guix profiles)
af12790b
RW
22 #:use-module ((guix build syscalls) #:select (mkdtemp!))
23 #:use-module (guix tests)
ed75bdf3
LC
24 #:use-module (guix store)
25 #:use-module ((guix grafts) #:select (%graft?))
26 #:use-module (guix derivations)
1fafc383 27 #:use-module (guix sets)
ed75bdf3 28 #:use-module (guix gexp)
45b90332
LC
29 #:use-module ((guix utils)
30 #:select (error-location? error-location location-line))
8ba7fd3c
LC
31 #:use-module ((guix build utils) #:select (which))
32 #:use-module (git)
33 #:use-module (guix git)
43badf26
LC
34 #:use-module (guix git-authenticate)
35 #:use-module (guix openpgp)
8ba7fd3c 36 #:use-module (guix tests git)
43badf26 37 #:use-module (guix tests gnupg)
af12790b 38 #:use-module (srfi srfi-1)
ed75bdf3 39 #:use-module (srfi srfi-26)
45b90332
LC
40 #:use-module (srfi srfi-34)
41 #:use-module (srfi srfi-35)
af12790b 42 #:use-module (srfi srfi-64)
43badf26
LC
43 #:use-module (rnrs bytevectors)
44 #:use-module (rnrs io ports)
872898f7 45 #:use-module (ice-9 control)
af12790b
RW
46 #:use-module (ice-9 match))
47
43badf26
LC
48(define (gpg+git-available?)
49 (and (which (git-command))
50 (which (gpg-command)) (which (gpgconf-command))))
51
52(define commit-id-string
53 (compose oid->string commit-id))
54
55\f
af12790b
RW
56(test-begin "channels")
57
58(define* (make-instance #:key
59 (name 'fake)
60 (commit "cafebabe")
61 (spec #f))
62 (define instance-dir (mkdtemp! "/tmp/checkout.XXXXXX"))
ce5d9ec8
LC
63 (when spec
64 (call-with-output-file (string-append instance-dir "/.guix-channel")
65 (lambda (port) (write spec port))))
ed75bdf3
LC
66 (checkout->channel-instance instance-dir
67 #:commit commit
68 #:name name))
af12790b
RW
69
70(define instance--boring (make-instance))
45b90332
LC
71(define instance--unsupported-version
72 (make-instance #:spec
73 '(channel (version 42) (dependencies whatever))))
af12790b
RW
74(define instance--no-deps
75 (make-instance #:spec
ce5d9ec8
LC
76 '(channel (version 0))))
77(define instance--sub-directory
78 (make-instance #:spec
79 '(channel (version 0) (directory "modules"))))
af12790b
RW
80(define instance--simple
81 (make-instance #:spec
82 '(channel
83 (version 0)
84 (dependencies
85 (channel
86 (name test-channel)
87 (url "https://example.com/test-channel"))))))
88(define instance--with-dupes
89 (make-instance #:spec
90 '(channel
91 (version 0)
92 (dependencies
93 (channel
94 (name test-channel)
95 (url "https://example.com/test-channel"))
96 (channel
97 (name test-channel)
98 (url "https://example.com/test-channel")
99 (commit "abc1234"))
100 (channel
101 (name test-channel)
102 (url "https://example.com/test-channel-elsewhere"))))))
103
45b90332
LC
104(define channel-instance-metadata
105 (@@ (guix channels) channel-instance-metadata))
ce5d9ec8
LC
106(define channel-metadata-directory
107 (@@ (guix channels) channel-metadata-directory))
108(define channel-metadata-dependencies
109 (@@ (guix channels) channel-metadata-dependencies))
af12790b
RW
110
111\f
ce5d9ec8
LC
112(test-equal "channel-instance-metadata returns default if .guix-channel does not exist"
113 '("/" ())
114 (let ((metadata (channel-instance-metadata instance--boring)))
115 (list (channel-metadata-directory metadata)
116 (channel-metadata-dependencies metadata))))
117
118(test-equal "channel-instance-metadata and default dependencies"
119 '()
120 (channel-metadata-dependencies (channel-instance-metadata instance--no-deps)))
121
122(test-equal "channel-instance-metadata and directory"
123 "/modules"
124 (channel-metadata-directory
125 (channel-instance-metadata instance--sub-directory)))
45b90332
LC
126
127(test-equal "channel-instance-metadata rejects unsupported version"
128 1 ;line number in the generated '.guix-channel'
129 (guard (c ((and (message-condition? c) (error-location? c))
130 (location-line (error-location c))))
131 (channel-instance-metadata instance--unsupported-version)))
af12790b 132
45b90332 133(test-assert "channel-instance-metadata returns <channel-metadata>"
af12790b 134 (every (@@ (guix channels) channel-metadata?)
45b90332 135 (map channel-instance-metadata
af12790b
RW
136 (list instance--no-deps
137 instance--simple
138 instance--with-dupes))))
139
45b90332 140(test-assert "channel-instance-metadata dependencies are channels"
af12790b 141 (let ((deps ((@@ (guix channels) channel-metadata-dependencies)
45b90332 142 (channel-instance-metadata instance--simple))))
af12790b
RW
143 (match deps
144 (((? channel? dep)) #t)
145 (_ #f))))
146
147(test-assert "latest-channel-instances includes channel dependencies"
148 (let* ((channel (channel
149 (name 'test)
150 (url "test")))
151 (test-dir (channel-instance-checkout instance--simple)))
053b10c3 152 (mock ((guix git) update-cached-checkout
8d1d5657 153 (lambda* (url #:key ref starting-commit)
af12790b 154 (match url
8d1d5657 155 ("test" (values test-dir "caf3cabba9e" #f))
053b10c3 156 (_ (values (channel-instance-checkout instance--no-deps)
8d1d5657 157 "abcde1234" #f)))))
053b10c3
LC
158 (with-store store
159 (let ((instances (latest-channel-instances store (list channel))))
160 (and (eq? 2 (length instances))
161 (lset= eq?
162 '(test test-channel)
163 (map (compose channel-name channel-instance-channel)
164 instances))))))))
af12790b
RW
165
166(test-assert "latest-channel-instances excludes duplicate channel dependencies"
167 (let* ((channel (channel
168 (name 'test)
169 (url "test")))
170 (test-dir (channel-instance-checkout instance--with-dupes)))
053b10c3 171 (mock ((guix git) update-cached-checkout
8d1d5657 172 (lambda* (url #:key ref starting-commit)
af12790b 173 (match url
8d1d5657 174 ("test" (values test-dir "caf3cabba9e" #f))
053b10c3 175 (_ (values (channel-instance-checkout instance--no-deps)
8d1d5657 176 "abcde1234" #f)))))
053b10c3
LC
177 (with-store store
178 (let ((instances (latest-channel-instances store (list channel))))
179 (and (= 2 (length instances))
180 (lset= eq?
181 '(test test-channel)
182 (map (compose channel-name channel-instance-channel)
183 instances))
184 ;; only the most specific channel dependency should remain,
185 ;; i.e. the one with a specified commit.
186 (find (lambda (instance)
187 (and (eq? (channel-name
188 (channel-instance-channel instance))
189 'test-channel)
190 (string=? (channel-commit
191 (channel-instance-channel instance))
192 "abc1234")))
193 instances)))))))
af12790b 194
872898f7
LC
195(unless (which (git-command)) (test-skip 1))
196(test-equal "latest-channel-instances #:validate-pull"
197 'descendant
198
199 ;; Make sure the #:validate-pull procedure receives the right values.
200 (let/ec return
201 (with-temporary-git-repository directory
202 '((add "a.txt" "A")
203 (commit "first commit")
204 (add "b.scm" "#t")
205 (commit "second commit"))
206 (with-repository directory repository
207 (let* ((commit1 (find-commit repository "first"))
208 (commit2 (find-commit repository "second"))
209 (spec (channel (url (string-append "file://" directory))
210 (name 'foo)))
211 (new (channel (inherit spec)
212 (commit (oid->string (commit-id commit2)))))
213 (old (channel (inherit spec)
214 (commit (oid->string (commit-id commit1))))))
215 (define (validate-pull channel current instance relation)
216 (return (and (eq? channel old)
217 (string=? (oid->string (commit-id commit2))
218 current)
219 (string=? (oid->string (commit-id commit1))
220 (channel-instance-commit instance))
221 relation)))
222
223 (with-store store
224 ;; Attempt a downgrade from NEW to OLD.
225 (latest-channel-instances store (list old)
226 #:current-channels (list new)
227 #:validate-pull validate-pull)))))))
228
ed75bdf3
LC
229(test-assert "channel-instances->manifest"
230 ;; Compute the manifest for a graph of instances and make sure we get a
231 ;; derivation graph that mirrors the instance graph. This test also ensures
232 ;; we don't try to access Git repositores at all at this stage.
233 (let* ((spec (lambda deps
234 `(channel (version 0)
235 (dependencies
236 ,@(map (lambda (dep)
237 `(channel
238 (name ,dep)
239 (url "http://example.org")))
240 deps)))))
241 (guix (make-instance #:name 'guix))
242 (instance0 (make-instance #:name 'a))
243 (instance1 (make-instance #:name 'b #:spec (spec 'a)))
244 (instance2 (make-instance #:name 'c #:spec (spec 'b)))
245 (instance3 (make-instance #:name 'd #:spec (spec 'c 'a))))
246 (%graft? #f) ;don't try to build stuff
247
248 ;; Create 'build-self.scm' so that GUIX is recognized as the 'guix' channel.
249 (let ((source (channel-instance-checkout guix)))
250 (mkdir (string-append source "/build-aux"))
251 (call-with-output-file (string-append source
252 "/build-aux/build-self.scm")
253 (lambda (port)
254 (write '(begin
255 (use-modules (guix) (gnu packages bootstrap))
256
257 (lambda _
258 (package->derivation %bootstrap-guile)))
259 port))))
260
261 (with-store store
262 (let ()
263 (define manifest
264 (run-with-store store
265 (channel-instances->manifest (list guix
266 instance0 instance1
267 instance2 instance3))))
268
269 (define entries
270 (manifest-entries manifest))
271
272 (define (depends? drv in out)
1fafc383
LC
273 ;; Return true if DRV depends (directly or indirectly) on all of IN
274 ;; and none of OUT.
275 (let ((set (list->set
276 (requisites store
277 (list (derivation-file-name drv)))))
ed75bdf3
LC
278 (in (map derivation-file-name in))
279 (out (map derivation-file-name out)))
1fafc383
LC
280 (and (every (cut set-contains? set <>) in)
281 (not (any (cut set-contains? set <>) out)))))
ed75bdf3
LC
282
283 (define (lookup name)
284 (run-with-store store
285 (lower-object
286 (manifest-entry-item
287 (manifest-lookup manifest
288 (manifest-pattern (name name)))))))
289
290 (let ((drv-guix (lookup "guix"))
291 (drv0 (lookup "a"))
292 (drv1 (lookup "b"))
293 (drv2 (lookup "c"))
294 (drv3 (lookup "d")))
295 (and (depends? drv-guix '() (list drv0 drv1 drv2 drv3))
296 (depends? drv0
297 (list) (list drv1 drv2 drv3))
298 (depends? drv1
299 (list drv0) (list drv2 drv3))
300 (depends? drv2
1fafc383 301 (list drv1) (list drv3))
ed75bdf3 302 (depends? drv3
1fafc383 303 (list drv2 drv0) (list))))))))
ed75bdf3 304
8ba7fd3c
LC
305(unless (which (git-command)) (test-skip 1))
306(test-equal "channel-news, no news"
307 '()
308 (with-temporary-git-repository directory
309 '((add "a.txt" "A")
310 (commit "the commit"))
311 (with-repository directory repository
312 (let ((channel (channel (url (string-append "file://" directory))
313 (name 'foo)))
314 (latest (reference-name->oid repository "HEAD")))
315 (channel-news-for-commit channel (oid->string latest))))))
316
317(unless (which (git-command)) (test-skip 1))
318(test-assert "channel-news, one entry"
319 (with-temporary-git-repository directory
320 `((add ".guix-channel"
321 ,(object->string
322 '(channel (version 0)
323 (news-file "news.scm"))))
324 (commit "first commit")
325 (add "src/a.txt" "A")
326 (commit "second commit")
9719e8d3 327 (tag "tag-for-first-news-entry")
8ba7fd3c
LC
328 (add "news.scm"
329 ,(lambda (repository)
330 (let ((previous
331 (reference-name->oid repository "HEAD")))
332 (object->string
333 `(channel-news
334 (version 0)
335 (entry (commit ,(oid->string previous))
336 (title (en "New file!")
337 (eo "Nova dosiero!"))
338 (body (en "Yeah, a.txt."))))))))
339 (commit "third commit")
340 (add "src/b.txt" "B")
341 (commit "fourth commit")
342 (add "news.scm"
343 ,(lambda (repository)
344 (let ((second
345 (commit-id
346 (find-commit repository "second commit")))
347 (previous
348 (reference-name->oid repository "HEAD")))
349 (object->string
350 `(channel-news
351 (version 0)
352 (entry (commit ,(oid->string previous))
353 (title (en "Another file!"))
354 (body (en "Yeah, b.txt.")))
9719e8d3 355 (entry (tag "tag-for-first-news-entry")
8ba7fd3c
LC
356 (title (en "Old news.")
357 (eo "Malnovaĵoj."))
358 (body (en "For a.txt"))))))))
359 (commit "fifth commit"))
360 (with-repository directory repository
361 (define (find-commit* message)
362 (oid->string (commit-id (find-commit repository message))))
363
364 (let ((channel (channel (url (string-append "file://" directory))
365 (name 'foo)))
366 (commit1 (find-commit* "first commit"))
367 (commit2 (find-commit* "second commit"))
368 (commit3 (find-commit* "third commit"))
369 (commit4 (find-commit* "fourth commit"))
370 (commit5 (find-commit* "fifth commit")))
371 ;; First try fetching all the news up to a given commit.
372 (and (null? (channel-news-for-commit channel commit2))
373 (lset= string=?
374 (map channel-news-entry-commit
375 (channel-news-for-commit channel commit5))
376 (list commit2 commit4))
377 (lset= equal?
378 (map channel-news-entry-title
379 (channel-news-for-commit channel commit5))
380 '((("en" . "Another file!"))
381 (("en" . "Old news.") ("eo" . "Malnovaĵoj."))))
382 (lset= string=?
383 (map channel-news-entry-commit
384 (channel-news-for-commit channel commit3))
385 (list commit2))
386
387 ;; Now fetch news entries that apply to a commit range.
388 (lset= string=?
389 (map channel-news-entry-commit
390 (channel-news-for-commit channel commit3 commit1))
391 (list commit2))
392 (lset= string=?
393 (map channel-news-entry-commit
394 (channel-news-for-commit channel commit5 commit3))
395 (list commit4))
396 (lset= string=?
397 (map channel-news-entry-commit
398 (channel-news-for-commit channel commit5 commit1))
9719e8d3
LC
399 (list commit4 commit2))
400 (lset= equal?
401 (map channel-news-entry-tag
402 (channel-news-for-commit channel commit5 commit1))
403 '(#f "tag-for-first-news-entry")))))))
8ba7fd3c 404
43badf26
LC
405(unless (gpg+git-available?) (test-skip 1))
406(test-assert "authenticate-channel, wrong first commit signer"
407 (with-fresh-gnupg-setup (list %ed25519-public-key-file
408 %ed25519-secret-key-file
409 %ed25519bis-public-key-file
410 %ed25519bis-secret-key-file)
411 (with-temporary-git-repository directory
412 `((add ".guix-channel"
413 ,(object->string
414 '(channel (version 0)
415 (keyring-reference "master"))))
416 (add ".guix-authorizations"
417 ,(object->string
418 `(authorizations (version 0)
419 ((,(key-fingerprint
420 %ed25519-public-key-file)
421 (name "Charlie"))))))
422 (add "signer.key" ,(call-with-input-file %ed25519-public-key-file
423 get-string-all))
424 (commit "first commit"
425 (signer ,(key-fingerprint %ed25519-public-key-file))))
426 (with-repository directory repository
427 (let* ((commit1 (find-commit repository "first"))
428 (intro ((@@ (guix channels) make-channel-introduction)
429 (commit-id-string commit1)
430 (openpgp-public-key-fingerprint
431 (read-openpgp-packet
432 %ed25519bis-public-key-file)) ;different key
433 #f)) ;no signature
434 (channel (channel (name 'example)
435 (url (string-append "file://" directory))
436 (introduction intro))))
437 (guard (c ((message? c)
438 (->bool (string-contains (condition-message c)
439 "initial commit"))))
440 (authenticate-channel channel directory
441 (commit-id-string commit1)
442 #:keyring-reference-prefix "")
443 'failed))))))
444
445(unless (gpg+git-available?) (test-skip 1))
446(test-assert "authenticate-channel, .guix-authorizations"
447 (with-fresh-gnupg-setup (list %ed25519-public-key-file
448 %ed25519-secret-key-file
449 %ed25519bis-public-key-file
450 %ed25519bis-secret-key-file)
451 (with-temporary-git-repository directory
452 `((add ".guix-channel"
453 ,(object->string
454 '(channel (version 0)
455 (keyring-reference "channel-keyring"))))
456 (add ".guix-authorizations"
457 ,(object->string
458 `(authorizations (version 0)
459 ((,(key-fingerprint
460 %ed25519-public-key-file)
461 (name "Charlie"))))))
462 (commit "zeroth commit")
463 (add "a.txt" "A")
464 (commit "first commit"
465 (signer ,(key-fingerprint %ed25519-public-key-file)))
466 (add "b.txt" "B")
467 (commit "second commit"
468 (signer ,(key-fingerprint %ed25519-public-key-file)))
469 (add "c.txt" "C")
470 (commit "third commit"
471 (signer ,(key-fingerprint %ed25519bis-public-key-file)))
472 (branch "channel-keyring")
473 (checkout "channel-keyring")
474 (add "signer.key" ,(call-with-input-file %ed25519-public-key-file
475 get-string-all))
476 (add "other.key" ,(call-with-input-file %ed25519bis-public-key-file
477 get-string-all))
478 (commit "keyring commit")
479 (checkout "master"))
480 (with-repository directory repository
481 (let* ((commit1 (find-commit repository "first"))
482 (commit2 (find-commit repository "second"))
483 (commit3 (find-commit repository "third"))
484 (intro ((@@ (guix channels) make-channel-introduction)
485 (commit-id-string commit1)
486 (openpgp-public-key-fingerprint
487 (read-openpgp-packet
488 %ed25519-public-key-file))
489 #f)) ;no signature
490 (channel (channel (name 'example)
491 (url (string-append "file://" directory))
492 (introduction intro))))
493 ;; COMMIT1 and COMMIT2 are fine.
494 (and (authenticate-channel channel directory
495 (commit-id-string commit2)
496 #:keyring-reference-prefix "")
497
498 ;; COMMIT3 is signed by an unauthorized key according to its
499 ;; parent's '.guix-authorizations' file.
500 (guard (c ((unauthorized-commit-error? c)
501 (and (oid=? (git-authentication-error-commit c)
502 (commit-id commit3))
503 (bytevector=?
504 (openpgp-public-key-fingerprint
505 (unauthorized-commit-error-signing-key c))
506 (openpgp-public-key-fingerprint
507 (read-openpgp-packet
508 %ed25519bis-public-key-file))))))
509 (authenticate-channel channel directory
510 (commit-id-string commit3)
511 #:keyring-reference-prefix "")
512 'failed)))))))
513
af12790b 514(test-end "channels")