import: utils: Trim patch version from names.
[jackhill/guix/guix.git] / guix / import / crate.scm
index f6057db..20efa13 100644 (file)
@@ -1,6 +1,7 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2016 David Craven <david@craven.ch>
-;;; Copyright © 2019 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2019, 2020 Martin Becze <mjbecze@riseup.net>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
   #:use-module ((guix download) #:prefix download:)
   #:use-module (gcrypt hash)
   #:use-module (guix http-client)
-  #:use-module (guix json)
   #:use-module (guix import json)
   #:use-module (guix import utils)
   #:use-module ((guix licenses) #:prefix license:)
+  #:use-module (guix memoization)
   #:use-module (guix monads)
   #:use-module (guix packages)
   #:use-module (guix upstream)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-2)
   #:use-module (srfi srfi-26)
+  #:use-module (srfi srfi-71)
   #:export (crate->guix-package
             guix-package->crate-name
+            string->license
+            crate-recursive-import
             %crate-updater))
 
 \f
   crate-dependency?
   json->crate-dependency
   (id            crate-dependency-id "crate_id")  ;string
-  (kind          crate-dependency-kind "kind"     ;'normal | 'dev
+  (kind          crate-dependency-kind "kind"     ;'normal | 'dev | 'build
                  string->symbol)
   (requirement   crate-dependency-requirement "req")) ;string
 
+(module-autoload! (current-module)
+                 '(semver) '(string->semver semver<?))
+(module-autoload! (current-module)
+                 '(semver ranges) '(string->semver-range semver-range-contains?))
+
 (define (lookup-crate name)
   "Look up NAME on https://crates.io and return the corresopnding <crate>
 record or #f if it was not found."
@@ -102,6 +111,8 @@ record or #f if it was not found."
                (json->crate `(,@alist
                               ("actual_versions" . ,versions))))))))
 
+(define lookup-crate* (memoize lookup-crate))
+
 (define (crate-version-dependencies version)
   "Return the list of <crate-dependency> records of VERSION, a
 <crate-version>."
@@ -109,7 +120,7 @@ record or #f if it was not found."
          (url  (string-append (%crate-base-url) path)))
     (match (assoc-ref (or (json-fetch url) '()) "dependencies")
       ((? vector? vector)
-       (map json->crate-dependency (vector->list vector)))
+       (delete-duplicates (map json->crate-dependency (vector->list vector))))
       (_
        '()))))
 
@@ -140,16 +151,22 @@ record or #f if it was not found."
      `((arguments (,'quasiquote ,args))))))
 
 (define* (make-crate-sexp #:key name version cargo-inputs cargo-development-inputs
-                          home-page synopsis description license
-                          #:allow-other-keys)
+                          home-page synopsis description license build?)
   "Return the `package' s-expression for a rust package with the given NAME,
 VERSION, CARGO-INPUTS, CARGO-DEVELOPMENT-INPUTS, HOME-PAGE, SYNOPSIS, DESCRIPTION,
 and LICENSE."
+  (define (format-inputs inputs)
+    (map
+     (match-lambda
+      ((name version)
+       (list (crate-name->package-name name)
+             (version-major+minor version))))
+     inputs))
+
   (let* ((port (http-fetch (crate-uri name version)))
          (guix-name (crate-name->package-name name))
-         (cargo-inputs (map crate-name->package-name cargo-inputs))
-         (cargo-development-inputs (map crate-name->package-name
-                                        cargo-development-inputs))
+         (cargo-inputs (format-inputs cargo-inputs))
+         (cargo-development-inputs (format-inputs cargo-development-inputs))
          (pkg `(package
                    (name ,guix-name)
                    (version ,version)
@@ -161,11 +178,14 @@ and LICENSE."
                               (base32
                                ,(bytevector->nix-base32-string (port-sha256 port))))))
                    (build-system cargo-build-system)
-                   ,@(maybe-arguments (append (maybe-cargo-inputs cargo-inputs)
+                   ,@(maybe-arguments (append (if build?
+                                                 '()
+                                                 '(#:skip-build? #t))
+                                              (maybe-cargo-inputs cargo-inputs)
                                               (maybe-cargo-development-inputs
                                                 cargo-development-inputs)))
                    (home-page ,(match home-page
-                                 (() "")
+                                 ('null "")
                                  (_ home-page)))
                    (synopsis ,synopsis)
                    (description ,(beautify-description description))
@@ -174,51 +194,106 @@ and LICENSE."
                                ((license) license)
                                (_ `(list ,@license)))))))
          (close-port port)
-         pkg))
+         (package->definition pkg #t)))
 
-(define %dual-license-rx
-  ;; Dual licensing is represented by a string such as "MIT OR Apache-2.0".
-  ;; This regexp matches that.
-  (make-regexp "^(.*) OR (.*)$"))
+(define (string->license string)
+  (filter-map (lambda (license)
+                (and (not (string-null? license))
+                     (not (any (lambda (elem) (string=? elem license))
+                               '("AND" "OR" "WITH")))
+                     (or (spdx-string->license license)
+                         'unknown-license!)))
+              (string-split string (string->char-set " /"))))
 
-(define (crate->guix-package crate-name)
+(define* (crate->guix-package crate-name #:key version include-dev-deps? repo)
   "Fetch the metadata for CRATE-NAME from crates.io, and return the
-`package' s-expression corresponding to that package, or #f on failure."
-  (define (string->license string)
-    (match (regexp-exec %dual-license-rx string)
-      (#f (list (spdx-string->license string)))
-      (m  (list (spdx-string->license (match:substring m 1))
-                (spdx-string->license (match:substring m 2))))))
+`package' s-expression corresponding to that package, or #f on failure.
+When VERSION is specified, convert it into a semver range and attempt to fetch
+the latest version matching this semver range; otherwise fetch the latest
+version of CRATE-NAME. If INCLUDE-DEV-DEPS is true then this will also
+look up the development dependencs for the given crate."
+
+  (define (semver-range-contains-string? range version)
+    (semver-range-contains? (string->semver-range range)
+                            (string->semver version)))
 
   (define (normal-dependency? dependency)
-    (eq? (crate-dependency-kind dependency) 'normal))
+    (or (eq? (crate-dependency-kind dependency) 'build)
+        (eq? (crate-dependency-kind dependency) 'normal)))
 
   (define crate
-    (lookup-crate crate-name))
-
-  (and crate
-       (let* ((version        (find (lambda (version)
-                                      (string=? (crate-version-number version)
-                                                (crate-latest-version crate)))
-                                    (crate-versions crate)))
-              (dependencies   (crate-version-dependencies version))
-              (dep-crates     (filter normal-dependency? dependencies))
-              (dev-dep-crates (remove normal-dependency? dependencies))
-              (cargo-inputs   (sort (map crate-dependency-id dep-crates)
-                                    string-ci<?))
-              (cargo-development-inputs
-               (sort (map crate-dependency-id dev-dep-crates)
-                     string-ci<?)))
-         (make-crate-sexp #:name crate-name
-                          #:version (crate-version-number version)
-                          #:cargo-inputs cargo-inputs
-                          #:cargo-development-inputs cargo-development-inputs
-                          #:home-page (or (crate-home-page crate)
-                                          (crate-repository crate))
-                          #:synopsis (crate-description crate)
-                          #:description (crate-description crate)
-                          #:license (and=> (crate-version-license version)
-                                           string->license)))))
+    (lookup-crate* crate-name))
+
+  (define version-number
+    (and crate
+         (or version
+             (crate-latest-version crate))))
+
+  ;; find the highest version of a crate that fulfills the semver <range>
+  (define (find-crate-version crate range)
+    (let* ((semver-range (string->semver-range range))
+           (versions
+            (sort
+             (filter (lambda (entry)
+                       (semver-range-contains? semver-range (first entry)))
+                     (map (lambda (ver)
+                            (list (string->semver (crate-version-number ver))
+                                  ver))
+                          (crate-versions crate)))
+             (match-lambda* (((semver _) ...)
+                             (apply semver<? semver))))))
+      (and (not (null-list? versions))
+           (second (last versions)))))
+
+  (define version*
+    (and crate
+         (find-crate-version crate version-number)))
+
+  ;; sort and map the dependencies to a list containing
+  ;; pairs of (name version)
+  (define (sort-map-dependencies deps)
+    (sort (map (lambda (dep)
+                 (let* ((name (crate-dependency-id dep))
+                        (crate (lookup-crate* name))
+                        (req (crate-dependency-requirement dep))
+                        (ver (find-crate-version crate req)))
+                   (list name
+                         (crate-version-number ver))))
+               deps)
+          (match-lambda* (((name _) ...)
+                          (apply string-ci<? name)))))
+
+  (and crate version*
+       (let* ((dependencies (crate-version-dependencies version*))
+              (dep-crates dev-dep-crates (partition normal-dependency? dependencies))
+              (cargo-inputs (sort-map-dependencies dep-crates))
+              (cargo-development-inputs (if include-dev-deps?
+                                            (sort-map-dependencies dev-dep-crates)
+                                            '())))
+         (values
+          (make-crate-sexp #:build? include-dev-deps?
+                           #:name crate-name
+                           #:version (crate-version-number version*)
+                           #:cargo-inputs cargo-inputs
+                           #:cargo-development-inputs cargo-development-inputs
+                           #:home-page (or (crate-home-page crate)
+                                           (crate-repository crate))
+                           #:synopsis (crate-description crate)
+                           #:description (crate-description crate)
+                           #:license (and=> (crate-version-license version*)
+                                            string->license))
+          (append cargo-inputs cargo-development-inputs)))))
+
+(define* (crate-recursive-import crate-name #:key version)
+  (recursive-import crate-name
+                    #:repo->guix-package (lambda* params
+                      ;; download development dependencies only for the top level package
+                      (let ((include-dev-deps? (equal? (car params) crate-name))
+                            (crate->guix-package* (memoize crate->guix-package)))
+                        (apply crate->guix-package*
+                               (append params `(#:include-dev-deps? ,include-dev-deps?)))))
+                    #:version version
+                    #:guix-name crate-name->package-name))
 
 (define (guix-package->crate-name package)
   "Return the crate name of PACKAGE."
@@ -232,23 +307,15 @@ and LICENSE."
       ((name _ ...) name))))
 
 (define (crate-name->package-name name)
-  (string-append "rust-" (string-join (string-split name #\_) "-")))
+  (guix-name "rust-" name))
 
 \f
 ;;;
 ;;; Updater
 ;;;
 
-(define (crate-package? package)
-  "Return true if PACKAGE is a Rust crate from crates.io."
-  (let ((source-url (and=> (package-source package) origin-uri))
-        (fetch-method (and=> (package-source package) origin-method)))
-    (and (eq? fetch-method download:url-fetch)
-         (match source-url
-           ((? string?)
-            (crate-url? source-url))
-           ((source-url ...)
-            (any crate-url? source-url))))))
+(define crate-package?
+  (url-predicate crate-url?))
 
 (define (latest-release package)
   "Return an <upstream-source> for the latest release of PACKAGE."