;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2014, 2015, 2016, 2017, 2018, 2019 Ludovic Courtès ;;; ;;; This file is part of GNU Guix. ;;; ;;; GNU Guix is free software; you can redistribute it and/or modify it ;;; under the terms of the GNU General Public License as published by ;;; the Free Software Foundation; either version 3 of the License, or (at ;;; your option) any later version. ;;; ;;; GNU Guix is distributed in the hope that it will be useful, but ;;; WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with GNU Guix. If not, see . (define-module (test-grafts) #:use-module (guix gexp) #:use-module (guix monads) #:use-module (guix derivations) #:use-module (guix store) #:use-module (guix utils) #:use-module (guix grafts) #:use-module (guix tests) #:use-module (gnu packages bootstrap) #:use-module (srfi srfi-1) #:use-module (srfi srfi-64) #:use-module (rnrs bytevectors) #:use-module (rnrs io ports) #:use-module (ice-9 vlist)) (define %store (open-connection-for-tests)) (define (bootstrap-binary name) (let ((bin (search-bootstrap-binary name (%current-system)))) (and %store (add-to-store %store name #t "sha256" bin)))) (define %bash (bootstrap-binary "bash")) (define %mkdir (bootstrap-binary "mkdir")) (test-begin "grafts") (test-equal "graft-derivation, grafted item is a direct dependency" '((type . graft) (graft (count . 2))) (let* ((build `(begin (mkdir %output) (chdir %output) (symlink %output "self") (call-with-output-file "text" (lambda (output) (format output "foo/~a/bar
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015, 2017, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (test-challenge)
  #:use-module (guix tests)
  #:use-module (guix tests http)
  #:use-module ((gcrypt hash) #:prefix gcrypt:)
  #:use-module (guix store)
  #:use-module (guix monads)
  #:use-module (guix derivations)
  #:use-module (guix serialization)
  #:use-module (guix packages)
  #:use-module (guix gexp)
  #:use-module (guix base32)
  #:use-module (guix narinfo)
  #:use-module (guix scripts challenge)
  #:use-module ((guix build utils) #:select (find-files))
  #:use-module (gnu packages bootstrap)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-64)
  #:use-module (rnrs bytevectors)
  #:use-module (rnrs io ports)
  #:use-module (ice-9 match))

(define query-path-hash*
  (store-lift query-path-hash))

(define (query-path-size item)
  (mlet %store-monad ((info (query-path-info* item)))
    (return (path-info-nar-size info))))

(define* (call-with-derivation-narinfo* drv thunk hash)
  (lambda (store)
    (with-derivation-narinfo drv (sha256 => hash)
      (values (run-with-store store (thunk)) store))))

(define-syntax with-derivation-narinfo*
  (syntax-rules (sha256 =>)
    ((_ drv (sha256 => hash) body ...)
     (call-with-derivation-narinfo* drv
       (lambda () body ...)
       hash))))


(test-begin "challenge")

(test-assertm "no discrepancies"
  (let ((text (random-text)))
    (mlet* %store-monad ((drv (gexp->derivation "something"
                                                #~(call-with-output-file
                                                      #$output
                                                    (lambda (port)
                                                      (display #$text port)))))
                         (out -> (derivation->output-path drv)))
      (mbegin %store-monad
        (built-derivations (list drv))
        (mlet %store-monad ((hash (query-path-hash* out)))
          (with-derivation-narinfo* drv (sha256 => hash)
            (>>= (compare-contents (list out) (%test-substitute-urls))
                 (match-lambda
                   ((report)
                    (return
                     (and (string=? out (comparison-report-item report))
                          (bytevector=?
                           (comparison-report-local-sha256 report)
                           hash)
                          (comparison-report-match? report))))))))))))

(test-assertm "one discrepancy"
  (let ((text (random-text)))
    (mlet* %store-monad ((drv (gexp->derivation "something"
                                                #~(call-with-output-file
                                                      #$output
                                                    (lambda (port)
                                                      (display #$text port)))))
                         (out -> (derivation->output-path drv)))
      (mbegin %store-monad
        (built-derivations (list drv))
        (mlet* %store-monad ((hash (query-path-hash* out))
                             (wrong-hash
                              -> (let* ((w (bytevector-copy hash))
                                        (b (bytevector-u8-ref w 0)))
                                   (bytevector-u8-set! w 0
                                                       (modulo (+ b 1) 128))
                                   w)))
          (with-derivation-narinfo* drv (sha256 => wrong-hash)
            (>>= (compare-contents (list out) (%test-substitute-urls))
                 (match-lambda
                   ((report)
                    (return
                     (and (string=? out (comparison-report-item (pk report)))
                          (eq? 'mismatch (comparison-report-result report))
                          (bytevector=? hash
                                        (comparison-report-local-sha256
                                         report))
                          (match (comparison-report-narinfos report)
                            ((bad)
                             (bytevector=? wrong-hash
                                           (narinfo-hash->sha256
                                            (narinfo-hash bad))))))))))))))))

(test-assertm "inconclusive: no substitutes"
  (mlet* %store-monad ((drv  (gexp->derivation "foo" #~(mkdir #$output)))
                       (out -> (derivation->output-path drv))
                       (_    (built-derivations (list drv)))
                       (hash (query-path-hash* out)))
    (>>= (compare-contents (list out) (%test-substitute-urls))
         (match-lambda
           ((report)
            (return
             (and (string=? out (comparison-report-item report))
                  (comparison-report-inconclusive? report)
                  (null? (comparison-report-narinfos report))
                  (bytevector=? (comparison-report-local-sha256 report)
                                hash))))))))

(test-assertm "inconclusive: no local build"
  (let ((text (random-text)))
    (mlet* %store-monad ((drv (gexp->derivation "something"
                                                #~(list #$output #$text)))
                         (out -> (derivation->output-path drv))
                         (hash -> (gcrypt:sha256 #vu8())))
      (with-derivation-narinfo* drv (sha256 => hash)
        (>>= (compare-contents (list out) (%test-substitute-urls))
             (match-lambda
               ((report)
                (return
                 (and (string=? out (comparison-report-item report))
                      (comparison-report-inconclusive? report)
                      (not (comparison-report-local-sha256 report))
                      (match (comparison-report-narinfos report)
                        ((narinfo)
                         (bytevector=? (narinfo-hash->sha256
                                        (narinfo-hash narinfo))
                                       hash))))))))))))
(define (make-narinfo item size hash)
  (format #f "StorePath: ~a
Compression: none
URL: nar/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-foo
NarSize: ~d
NarHash: sha256:~a
References: ~%" item size (bytevector->nix-base32-string hash)))

(define (call-mismatch-test proc)
  "Pass PROC a <comparison-report> for a mismatch and return its return
value."

  ;; Pretend we have two different results for the same store item, ITEM, with
  ;; "/bin/guile" differing between the two nars.
  (mlet* %store-monad
      ((drv1 (package->derivation %bootstrap-guile))
       (drv2 (gexp->derivation
              "broken-guile"
              (with-imported-modules '((guix build utils))
                #~(begin
                    (use-modules (guix build utils))
                    (copy-recursively #$drv1 #$output)
                    (chmod (string-append #$output "/bin/guile")
                           #o755)
                    (call-with-output-file (string-append
                                            #$output
                                            "/bin/guile")
                      (lambda (port)
                        (display "corrupt!" port)))))))
       (out1 -> (derivation->output-path drv1))
       (out2 -> (derivation->output-path drv2))
       (item -> (string-append (%store-prefix) "/"
                               (bytevector->nix-base32-string
                                (random-bytevector 32))
                               "-foo"
                               (number->string (current-time) 16))))
    (mbegin %store-monad
      (built-derivations (list drv1 drv2))
      (mlet* %store-monad ((size1 (query-path-size out1))
                           (size2 (query-path-size out2))
                           (hash1 (query-path-hash* out1))
                           (hash2 (query-path-hash* out2))
                           (nar1 -> (call-with-bytevector-output-port
                                     (lambda (port)
                                       (write-file out1 port))))
                           (nar2 -> (call-with-bytevector-output-port
                                     (lambda (port)
                                       (write-file out2 port)))))
        (parameterize ((%http-server-port 9000))
          (with-http-server `((200 ,(make-narinfo item size1 hash1))
                              (200 ,nar1))
            (parameterize ((%http-server-port 9001))
              (with-http-server `((200 ,(make-narinfo item size2 hash2))
                                  (200 ,nar2))
                (mlet* %store-monad ((urls -> (list (%local-url 9000)
                                                    (%local-url 9001)))
                                     (reports (compare-contents (list item)
                                                                urls)))
                  (pk 'report reports)
                  (return (proc (car reports))))))))))))

(test-assertm "differing-files"
  (call-mismatch-test
   (lambda (report)
     (equal? (differing-files report) '("/bin/guile")))))

(test-assertm "call-with-mismatches"
  (call-mismatch-test
   (lambda (report)
     (call-with-mismatches
      report
      (lambda (directory1 directory2)
        (let* ((files1 (find-files directory1))
               (files2 (find-files directory2))
               (files  (map (cute string-drop <> (string-length directory1))
                            files1)))
          (and (equal? files
                       (map (cute string-drop <> (string-length directory2))
                            files2))
               (equal? (remove (lambda (file)
                                 (file=? (string-append directory1 "/" file)
                                         (string-append directory2 "/" file)))
                               files)
                       '("/bin/guile")))))))))

(test-end)

;;; Local Variables:
;;; eval: (put 'with-derivation-narinfo* 'scheme-indent-function 2)
;;; End:
p1r (build-expression->derivation %store "P1" `(let ((other (assoc-ref %outputs "ONE"))) (mkdir other) (call-with-output-file (string-append other "/replacement") (const #t))) #:outputs '("ONE"))) (p2 (build-expression->derivation %store "p2" `(let ((aaa (assoc-ref %outputs "aaa")) (zzz (assoc-ref %outputs "zzz"))) (mkdir zzz) (chdir zzz) (mkdir aaa) (chdir aaa) (symlink (assoc-ref %build-inputs "p1:two") "two")) #:outputs '("aaa" "zzz") #:inputs `(("p1:one" ,p1 "one") ("p1:two" ,p1 "two")))) (p1g (graft (origin p1) (origin-output "one") (replacement p1r) (replacement-output "ONE"))) (p2g (graft-derivation %store p2 (list p1g) #:outputs '("aaa")))) ;; P2:aaa depends on P1:two, but not on P1:one, so nothing to graft. (eq? p2g p2))) (test-equal "graft-derivation, unused outputs not depended on" '("aaa") ;; Make sure that the result of 'graft-derivation' does not pull outputs ;; that are irrelevant to the grafting process. See ;; . (let* ((p1 (build-expression->derivation %store "p1" `(let ((one (assoc-ref %outputs "one")) (two (assoc-ref %outputs "two"))) (mkdir one) (mkdir two)) #:outputs '("one" "two"))) (p1r (build-expression->derivation %store "P1" `(let ((other (assoc-ref %outputs "ONE"))) (mkdir other) (call-with-output-file (string-append other "/replacement") (const #t))) #:outputs '("ONE"))) (p2 (build-expression->derivation %store "p2" `(let ((aaa (assoc-ref %outputs "aaa")) (zzz (assoc-ref %outputs "zzz"))) (mkdir zzz) (chdir zzz) (symlink (assoc-ref %build-inputs "p1:two") "two") (mkdir aaa) (chdir aaa) (symlink (assoc-ref %build-inputs "p1:one") "one")) #:outputs '("aaa" "zzz") #:inputs `(("p1:one" ,p1 "one") ("p1:two" ,p1 "two")))) (p1g (graft (origin p1) (origin-output "one") (replacement p1r) (replacement-output "ONE"))) (p2g (graft-derivation %store p2 (list p1g) #:outputs '("aaa")))) ;; Here P2G should only depend on P1:one and P1R:one; it must not depend ;; on P1:two or P1R:two since these are unused in the grafting process. (and (not (eq? p2g p2)) (let* ((inputs (derivation-inputs p2g)) (match-input (lambda (drv) (lambda (input) (string=? (derivation-input-path input) (derivation-file-name drv))))) (p1-inputs (filter (match-input p1) inputs)) (p1r-inputs (filter (match-input p1r) inputs)) (p2-inputs (filter (match-input p2) inputs))) (and (equal? p1-inputs (list (derivation-input p1 '("one")))) (equal? p1r-inputs (list (derivation-input p1r '("ONE")))) (equal? p2-inputs (list (derivation-input p2 '("aaa")))) (derivation-output-names p2g)))))) (test-assert "graft-derivation, renaming" ; (let* ((build `(begin (use-modules (guix build utils)) (mkdir-p (string-append (assoc-ref %outputs "out") "/" (assoc-ref %build-inputs "in"))))) (orig (build-expression->derivation %store "thing-to-graft" build #:modules '((guix build utils)) #:inputs `(("in" ,%bash)))) (repl (add-text-to-store %store "bash" "fake bash")) (grafted (graft-derivation %store orig (list (graft (origin %bash) (replacement repl)))))) (and (build-derivations %store (list grafted)) (let ((out (derivation->output-path grafted))) (file-is-directory? (string-append out "/" repl)))))) (test-assert "graft-derivation, grafts are not shadowed" ;; We build a DAG as below, where dotted arrows represent replacements and ;; solid arrows represent dependencies: ;; ;; P1 ·············> P1R ;; |\__________________. ;; v v ;; P2 ·············> P2R ;; | ;; v ;; P3 ;; ;; We want to make sure that the two grafts we want to apply to P3 are ;; honored and not shadowed by other computed grafts. (let* ((p1 (build-expression->derivation %store "p1" '(mkdir (assoc-ref %outputs "out")))) (p1r (build-expression->derivation %store "P1" '(let ((out (assoc-ref %outputs "out"))) (mkdir out) (call-with-output-file (string-append out "/replacement") (const #t))))) (p2 (build-expression->derivation %store "p2" `(let ((out (assoc-ref %outputs "out"))) (mkdir out) (chdir out) (symlink (assoc-ref %build-inputs "p1") "p1")) #:inputs `(("p1" ,p1)))) (p2r (build-expression->derivation %store "P2" `(let ((out (assoc-ref %outputs "out"))) (mkdir out) (chdir out) (symlink (assoc-ref %build-inputs "p1") "p1") (call-with-output-file (string-append out "/replacement") (const #t))) #:inputs `(("p1" ,p1)))) (p3 (build-expression->derivation %store "p3" `(let ((out (assoc-ref %outputs "out"))) (mkdir out) (chdir out) (symlink (assoc-ref %build-inputs "p2") "p2")) #:inputs `(("p2" ,p2)))) (p1g (graft (origin p1) (replacement p1r))) (p2g (graft (origin p2) (replacement (graft-derivation %store p2r (list p1g))))) (p3d (graft-derivation %store p3 (list p1g p2g)))) (and (build-derivations %store (list p3d)) (let ((out (derivation->output-path (pk p3d)))) ;; Make sure OUT refers to the replacement of P2, which in turn ;; refers to the replacement of P1, as specified by P1G and P2G. ;; It used to be the case that P2G would be shadowed by a simple ;; P2->P2R graft, which is not what we want. (and (file-exists? (string-append out "/p2/replacement")) (file-exists? (string-append out "/p2/p1/replacement"))))))) (define buffer-size ;; Must be equal to REQUEST-SIZE in 'replace-store-references'. (expt 2 20)) (test-equal "replace-store-references, " (string-append (make-string (- buffer-size 47) #\a) "/gnu/store/" (make-string 32 #\8) "-SoMeTHiNG" (list->string (map integer->char (iota 77 33)))) ;; Create input data where the right-hand-size of the dash ("-something" ;; here) goes beyond the end of the internal buffer of ;; 'replace-store-references'. (let* ((content (string-append (make-string (- buffer-size 47) #\a) "/gnu/store/" (make-string 32 #\7) "-something" (list->string (map integer->char (iota 77 33))))) (replacement (alist->vhash `((,(make-string 32 #\7) . ,(string->utf8 (string-append (make-string 32 #\8) "-SoMeTHiNG"))))))) (call-with-output-string (lambda (output) ((@@ (guix build graft) replace-store-references) (open-input-string content) output replacement "/gnu/store"))))) (test-end)