Subject: [bug#36048] [PATCH] guix: import: hackage: handle hackage revisions

From: Timothy Sample <[email-masked]>
Date: Wed, 12 Jun 2019 22:28:35 -0400

Hi Robert,

Thanks for the patch, and thanks for the work you’ve been doing in
general on the Haskell packages and tools in Guix!

Robert Vollmert <[email-masked]> writes:

> Hackage packages can have metadata revision (cabal-file only)
> that aren't reflected in the source archive. haskell-build-system
> has support for this, but previously `guix import hackage` would
> create a definition based on the new cabal file but building using
> the old cabal file.
>
> Compare https://debbugs.gnu.org/cgi/bugreport.cgi?bug=35750.
>
>
> * guix/import/cabal.scm: Parse `x-revision:` property.
> * guix/import/hackage.scm: Compute hash of cabal file, and write
> cabal-revision build system arguments.
> * guix/tests/hackage.scm: Test import of cabal revision.
> ---
>  guix/import/cabal.scm   |  7 +++--
>  guix/import/hackage.scm | 61 ++++++++++++++++++++++++++++++-----------
>  tests/hackage.scm       | 46 +++++++++++++++++++++++++++++++
>  3 files changed, 96 insertions(+), 18 deletions(-)
>
> diff --git a/guix/import/cabal.scm b/guix/import/cabal.scm
> index 1a87be0b00..7dfe771e41 100644
> --- a/guix/import/cabal.scm
> +++ b/guix/import/cabal.scm
> @@ -40,6 +40,7 @@
>              cabal-package?
>              cabal-package-name
>              cabal-package-version
> +            cabal-package-revision
>              cabal-package-license
>              cabal-package-home-page
>              cabal-package-source-repository
> @@ -638,13 +639,14 @@ If #f use the function 'port-filename' to obtain it."
>  ;; information of the Cabal file, but only the ones we currently are
>  ;; interested in.
>  (define-record-type <cabal-package>
> -  (make-cabal-package name version license home-page source-repository
> +  (make-cabal-package name version revision license home-page source-repository
>                        synopsis description
>                        executables lib test-suites
>                        flags eval-environment custom-setup)
>    cabal-package?
>    (name   cabal-package-name)
>    (version cabal-package-version)
> +  (revision cabal-package-revision)
>    (license cabal-package-license)
>    (home-page cabal-package-home-page)
>    (source-repository cabal-package-source-repository)
> @@ -838,6 +840,7 @@ See the manual for limitations.")))))))
>    (define (cabal-evaluated-sexp->package evaluated-sexp)
>      (let* ((name (lookup-join evaluated-sexp "name"))
>             (version (lookup-join evaluated-sexp "version"))
> +           (revision (lookup-join evaluated-sexp "x-revision"))
>             (license (lookup-join evaluated-sexp "license"))
>             (home-page (lookup-join evaluated-sexp "homepage"))
>             (home-page-or-hackage
> @@ -856,7 +859,7 @@ See the manual for limitations.")))))))
>             (custom-setup (match (make-cabal-section evaluated-sexp 'custom-setup)
>                             ((x) x)
>                             (_ #f))))
> -      (make-cabal-package name version license home-page-or-hackage
> +      (make-cabal-package name version revision license home-page-or-hackage
>                            source-repository synopsis description executables lib
>                            test-suites flags eval-environment custom-setup)))
>  
> diff --git a/guix/import/hackage.scm b/guix/import/hackage.scm
> index 366256b40d..cf8219143a 100644
> --- a/guix/import/hackage.scm
> +++ b/guix/import/hackage.scm
> @@ -117,9 +117,15 @@ version is returned."
>            (#f name)
>            (m (match:substring m 1)))))))
>  
> -(define (hackage-fetch name-version)
> -  "Return the Cabal file for the package NAME-VERSION, or #f on failure.  If
> -the version part is omitted from the package name, then return the latest
> +(define (read-cabal-and-hash port)

This procedure should have a docstring.

> +  (let-values (((port get-hash) (open-sha256-input-port port)))
> +    (cons
> +      (read-cabal (canonical-newline-port port))
> +      (bytevector->nix-base32-string (get-hash)))))

The indentation here is wrong.  In general, list elements all line up
with each other, so everything would be under the ‘c’ of “cons”.  There
are exceptions to this, but they’re mostly for special block-like forms
such as “let” and “begin”.  However, I think Schemers tend to avoid
putting a line break after the procedure when applying.  That is, it
would be more conventionally formatted as:

    (cons (read-cabal (canonical-newline-port port))
          (bytevector->nix-base32-string (get-hash)))

Also, I think returning multiple values would be more natural here
(i.e., replace “cons” with “values”).

> +
> +(define (hackage-fetch-and-hash name-version)
> +  "Return the Cabal file and hash for the package NAME-VERSION, or #f on failure.

Here, it would be clearer to mention the shape of the return value as
well.  For the “values” version, it would be something like “Return two
values: the Cabal file for the package NAME-VERSION and its hash....”  I
cribbed this wording from the Guile manual, but I worry that the
referent of that last “its” is not clear.  It’s probably good enough,
but maybe you can do better.  ;)  It’s the “two values” part that’s
important.

Also, please try to keep the line lengths under 80 columns.

> +If the version part is omitted from the package name, then return the latest
>  version."
>    (guard (c ((and (http-get-error? c)
>                    (= 404 (http-get-error-code c)))
> @@ -127,10 +133,18 @@ version."
>      (let-values (((name version) (package-name->name+version name-version)))
>        (let* ((url (hackage-cabal-url name version))
>               (port (http-fetch url))
> -             (result (read-cabal (canonical-newline-port port))))
> +             (result (read-cabal-and-hash port)))
>          (close-port port)
>          result))))
>  
> +(define (hackage-fetch name-version)
> +  "Return the Cabal file for the package NAME-VERSION, or #f on failure.  If
> +the version part is omitted from the package name, then return the latest
> +version."
> +  (match (hackage-fetch-and-hash name-version)
> +         ((cabal . hash) cabal)
> +         (_              #f)))
> +

This will have to become a “let-values” form.

>  (define string->license
>    ;; List of valid values from
>    ;; https://www.haskell.org
> @@ -198,15 +212,19 @@ package being processed and is used to filter references to itself."
>                                     (cons own-name ghc-standard-libraries))))
>            dependencies))
>  
> -(define* (hackage-module->sexp cabal #:key (include-test-dependencies? #t))
> +(define* (hackage-module->sexp cabal cabal-hash #:key (include-test-dependencies? #t))

Another long line.  You could line up “#:key” under the first “cabal”.

>    "Return the `package' S-expression for a Cabal package.  CABAL is the
> -representation of a Cabal file as produced by 'read-cabal'."
> +representation of a Cabal file as produced by 'read-cabal'. CABAL-HASH is

There should be two spaces after a period  ------------------^

> +the hash of the Cabal file."
>  
>    (define name
>      (cabal-package-name cabal))
>  
>    (define version
>      (cabal-package-version cabal))
> +
> +  (define revision
> +    (cabal-package-revision cabal))
>    
>    (define source-url
>      (hackage-source-url name version))
> @@ -252,9 +270,17 @@ representation of a Cabal file as produced by 'read-cabal'."
>                     (list 'quasiquote inputs))))))
>    
>    (define (maybe-arguments)
> -    (if (not include-test-dependencies?)
> -        '((arguments `(#:tests? #f)))
> -        '()))
> +    (define testargs (if (not include-test-dependencies?)
> +                       '(#:tests? #f)
> +                       '()))
> +    (define revargs  (if (not (string-null? revision))
> +                       `(#:cabal-revision (,revision ,cabal-hash))
> +                       '()))
> +    (define args (append testargs revargs))
> +    (if (not (nil? args))
> +      (let ((qargs `(,'quasiquote ,args)))
> +        `((arguments ,qargs)))
> +      '()))

I think that this would be a little clearer using “match” and without
the intermediary definitions:

    (match (append (if (not include-test-dependencies?)
                       '(#:tests? #f)
                       '())
                   (if (not (string-null? revision))
                       `(#:cabal-revision (,revision ,cabal-hash))
                       '()))
      (() '())
      (args `((arguments (,'quasiquote ,args)))))

>  
>    (let ((tarball (with-store store
>                     (download-to-store store source-url))))
> @@ -294,13 +320,16 @@ symbol 'true' or 'false'.  The value associated with other keys has to conform
>  to the Cabal file format definition.  The default value associated with the
>  keys \"os\", \"arch\" and \"impl\" is \"linux\", \"x86_64\" and \"ghc\"
>  respectively."
> -  (let ((cabal-meta (if port
> -                        (read-cabal (canonical-newline-port port))
> -                        (hackage-fetch package-name))))
> -    (and=> cabal-meta (compose (cut hackage-module->sexp <>
> -                                    #:include-test-dependencies?
> -                                    include-test-dependencies?)
> -                               (cut eval-cabal <> cabal-environment)))))
> +  (match
> +    (if port (read-cabal-and-hash port)
> +             (hackage-fetch-and-hash package-name))
> +    ((cabal-meta . cabal-hash)
> +     (and=> cabal-meta (compose (cut hackage-module->sexp <>
> +                                     cabal-hash
> +                                     #:include-test-dependencies?
> +                                     include-test-dependencies?)
> +                                (cut eval-cabal <> cabal-environment))))
> +    (_ #f)))

This will also need to be a “let-values” form for the multiple values.
Fortunately, it will look almost identical to the old “let” form, so
that’s kinda nice (if you’re as easily amused as I am).  :)

>  (define hackage->guix-package/m                   ;memoized variant
>    (memoize hackage->guix-package))
> diff --git a/tests/hackage.scm b/tests/hackage.scm
> index 38a5825af7..fe4e0efb69 100644
> --- a/tests/hackage.scm
> +++ b/tests/hackage.scm
> @@ -274,6 +274,52 @@ executable cabal
>  (test-assert "hackage->guix-package test multiline desc (braced)"
>    (eval-test-with-cabal test-cabal-multiline-braced match-ghc-foo))
>  
> +;; test hackage cabal revisions

To be consistent with the other comments in the file, I would suggest:

    ;; Check Hackage Cabal revisions.

(I know that some of the comments are missing a period at the end, but
most of them have it, and it should be there.)

> +(define test-cabal-revision
> +  "name: foo
> +version: 1.0.0
> +x-revision: 2
> +homepage: http://test.org
> +synopsis: synopsis
> +description: description
> +license: BSD3
> +executable cabal
> +  build-depends:
> +    HTTP       >= 4000.2.5 && < 4000.3,
> +    mtl        >= 2.0      && < 3
> +")
> +
> +(define-package-matcher match-ghc-foo-revision
> +  ('package
> +    ('name "ghc-foo")
> +    ('version "1.0.0")
> +    ('source
> +     ('origin
> +       ('method 'url-fetch)
> +       ('uri ('string-append
> +              "https://hackage.haskell.org/package/foo/foo-"
> +              'version
> +              ".tar.gz"))
> +       ('sha256
> +        ('base32
> +         (? string? hash)))))
> +    ('build-system 'haskell-build-system)
> +    ('inputs
> +     ('quasiquote
> +      (("ghc-http" ('unquote 'ghc-http)))))
> +    ('arguments
> +     ('quasiquote
> +      ('#:cabal-revision
> +       ("2" "0xxd88fb659f0krljidbvvmkh9ppjnx83j0nqzx8whcg4n5qbyng"))))
> +    ('home-page "http://test.org")
> +    ('synopsis (? string?))
> +    ('description (? string?))
> +    ('license 'bsd-3)))
> +
> +(test-assert "hackage->guix-package test cabal revision"
> +  (eval-test-with-cabal test-cabal-revision match-ghc-foo-revision))
> +
> +

I think there’s an extra line break here.

>  (test-assert "read-cabal test 1"
>    (match (call-with-input-string test-read-cabal-1 read-cabal)
>      ((("name" ("test-me"))

Overall, it looks pretty good.  Bonus points for including a test!  With
the few tweaks I mentioned, I think it will be ready to go.  Thanks
again.

-- Tim