import: cpan: Rewrite to use 'define-json-mapping'.
[jackhill/guix/guix.git] / guix / import / crate.scm
1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2016 David Craven <david@craven.ch>
3 ;;; Copyright © 2019 Ludovic Courtès <ludo@gnu.org>
4 ;;; Copyright © 2019 Martin Becze <mjbecze@riseup.net>
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 import crate)
22 #:use-module (guix base32)
23 #:use-module (guix build-system cargo)
24 #:use-module ((guix download) #:prefix download:)
25 #:use-module (gcrypt hash)
26 #:use-module (guix http-client)
27 #:use-module (guix json)
28 #:use-module (guix import json)
29 #:use-module (guix import utils)
30 #:use-module ((guix licenses) #:prefix license:)
31 #:use-module (guix monads)
32 #:use-module (guix packages)
33 #:use-module (guix upstream)
34 #:use-module (guix utils)
35 #:use-module (ice-9 match)
36 #:use-module (ice-9 regex)
37 #:use-module (json)
38 #:use-module (srfi srfi-1)
39 #:use-module (srfi srfi-2)
40 #:use-module (srfi srfi-26)
41 #:export (crate->guix-package
42 guix-package->crate-name
43 crate-recursive-import
44 %crate-updater))
45
46 \f
47 ;;;
48 ;;; Interface to https://crates.io/api/v1.
49 ;;;
50
51 ;; Crates. A crate is essentially a "package". It can have several
52 ;; "versions", each of which has its own set of dependencies, license,
53 ;; etc.--see <crate-version> below.
54 (define-json-mapping <crate> make-crate crate?
55 json->crate
56 (name crate-name) ;string
57 (latest-version crate-latest-version "max_version") ;string
58 (home-page crate-home-page "homepage") ;string | #nil
59 (repository crate-repository) ;string
60 (description crate-description) ;string
61 (keywords crate-keywords ;list of strings
62 "keywords" vector->list)
63 (categories crate-categories ;list of strings
64 "categories" vector->list)
65 (versions crate-versions "actual_versions" ;list of <crate-version>
66 (lambda (vector)
67 (map json->crate-version
68 (vector->list vector))))
69 (links crate-links)) ;alist
70
71 ;; Crate version.
72 (define-json-mapping <crate-version> make-crate-version crate-version?
73 json->crate-version
74 (id crate-version-id) ;integer
75 (number crate-version-number "num") ;string
76 (download-path crate-version-download-path "dl_path") ;string
77 (readme-path crate-version-readme-path "readme_path") ;string
78 (license crate-version-license "license") ;string
79 (links crate-version-links)) ;alist
80
81 ;; Crate dependency. Each dependency (each edge in the graph) is annotated as
82 ;; being a "normal" dependency or a development dependency. There also
83 ;; information about the minimum required version, such as "^0.0.41".
84 (define-json-mapping <crate-dependency> make-crate-dependency
85 crate-dependency?
86 json->crate-dependency
87 (id crate-dependency-id "crate_id") ;string
88 (kind crate-dependency-kind "kind" ;'normal | 'dev
89 string->symbol)
90 (requirement crate-dependency-requirement "req")) ;string
91
92 (define (lookup-crate name)
93 "Look up NAME on https://crates.io and return the corresopnding <crate>
94 record or #f if it was not found."
95 (let ((json (json-fetch (string-append (%crate-base-url) "/api/v1/crates/"
96 name))))
97 (and=> (and json (assoc-ref json "crate"))
98 (lambda (alist)
99 ;; The "versions" field of ALIST is simply a list of version IDs
100 ;; (integers). Here, we squeeze in the actual version
101 ;; dictionaries that are not part of ALIST but are just more
102 ;; convenient handled this way.
103 (let ((versions (or (assoc-ref json "versions") '#())))
104 (json->crate `(,@alist
105 ("actual_versions" . ,versions))))))))
106
107 (define (crate-version-dependencies version)
108 "Return the list of <crate-dependency> records of VERSION, a
109 <crate-version>."
110 (let* ((path (assoc-ref (crate-version-links version) "dependencies"))
111 (url (string-append (%crate-base-url) path)))
112 (match (assoc-ref (or (json-fetch url) '()) "dependencies")
113 ((? vector? vector)
114 (map json->crate-dependency (vector->list vector)))
115 (_
116 '()))))
117
118 \f
119 ;;;
120 ;;; Converting crates to Guix packages.
121 ;;;
122
123 (define (maybe-cargo-inputs package-names)
124 (match (package-names->package-inputs package-names)
125 (()
126 '())
127 ((package-inputs ...)
128 `(#:cargo-inputs ,package-inputs))))
129
130 (define (maybe-cargo-development-inputs package-names)
131 (match (package-names->package-inputs package-names)
132 (()
133 '())
134 ((package-inputs ...)
135 `(#:cargo-development-inputs ,package-inputs))))
136
137 (define (maybe-arguments arguments)
138 (match arguments
139 (()
140 '())
141 ((args ...)
142 `((arguments (,'quasiquote ,args))))))
143
144 (define* (make-crate-sexp #:key name version cargo-inputs cargo-development-inputs
145 home-page synopsis description license
146 #:allow-other-keys)
147 "Return the `package' s-expression for a rust package with the given NAME,
148 VERSION, CARGO-INPUTS, CARGO-DEVELOPMENT-INPUTS, HOME-PAGE, SYNOPSIS, DESCRIPTION,
149 and LICENSE."
150 (let* ((port (http-fetch (crate-uri name version)))
151 (guix-name (crate-name->package-name name))
152 (cargo-inputs (map crate-name->package-name cargo-inputs))
153 (cargo-development-inputs (map crate-name->package-name
154 cargo-development-inputs))
155 (pkg `(package
156 (name ,guix-name)
157 (version ,version)
158 (source (origin
159 (method url-fetch)
160 (uri (crate-uri ,name version))
161 (file-name (string-append name "-" version ".tar.gz"))
162 (sha256
163 (base32
164 ,(bytevector->nix-base32-string (port-sha256 port))))))
165 (build-system cargo-build-system)
166 ,@(maybe-arguments (append (maybe-cargo-inputs cargo-inputs)
167 (maybe-cargo-development-inputs
168 cargo-development-inputs)))
169 (home-page ,(match home-page
170 (() "")
171 (_ home-page)))
172 (synopsis ,synopsis)
173 (description ,(beautify-description description))
174 (license ,(match license
175 (() #f)
176 ((license) license)
177 (_ `(list ,@license)))))))
178 (close-port port)
179 pkg))
180
181 (define (string->license string)
182 (filter-map (lambda (license)
183 (and (not (string-null? license))
184 (not (any (lambda (elem) (string=? elem license))
185 '("AND" "OR" "WITH")))
186 (or (spdx-string->license license)
187 'unknown-license!)))
188 (string-split string (string->char-set " /"))))
189
190 (define* (crate->guix-package crate-name #:optional version)
191 "Fetch the metadata for CRATE-NAME from crates.io, and return the
192 `package' s-expression corresponding to that package, or #f on failure.
193 When VERSION is specified, attempt to fetch that version; otherwise fetch the
194 latest version of CRATE-NAME."
195
196 (define (normal-dependency? dependency)
197 (eq? (crate-dependency-kind dependency) 'normal))
198
199 (define crate
200 (lookup-crate crate-name))
201
202 (define version-number
203 (or version
204 (crate-latest-version crate)))
205
206 (define version*
207 (find (lambda (version)
208 (string=? (crate-version-number version)
209 version-number))
210 (crate-versions crate)))
211
212 (and crate version*
213 (let* ((dependencies (crate-version-dependencies version*))
214 (dep-crates (filter normal-dependency? dependencies))
215 (dev-dep-crates (remove normal-dependency? dependencies))
216 (cargo-inputs (sort (map crate-dependency-id dep-crates)
217 string-ci<?))
218 (cargo-development-inputs
219 (sort (map crate-dependency-id dev-dep-crates)
220 string-ci<?)))
221 (values
222 (make-crate-sexp #:name crate-name
223 #:version (crate-version-number version*)
224 #:cargo-inputs cargo-inputs
225 #:cargo-development-inputs cargo-development-inputs
226 #:home-page (or (crate-home-page crate)
227 (crate-repository crate))
228 #:synopsis (crate-description crate)
229 #:description (crate-description crate)
230 #:license (and=> (crate-version-license version*)
231 string->license))
232 (append cargo-inputs cargo-development-inputs)))))
233
234 (define* (crate-recursive-import crate-name #:optional version)
235 (recursive-import crate-name #f
236 #:repo->guix-package
237 (lambda (name repo)
238 (let ((version (and (string=? name crate-name)
239 version)))
240 (crate->guix-package name version)))
241 #:guix-name crate-name->package-name))
242
243 (define (guix-package->crate-name package)
244 "Return the crate name of PACKAGE."
245 (and-let* ((origin (package-source package))
246 (uri (origin-uri origin))
247 (crate-url? uri)
248 (len (string-length crate-url))
249 (path (xsubstring uri len))
250 (parts (string-split path #\/)))
251 (match parts
252 ((name _ ...) name))))
253
254 (define (crate-name->package-name name)
255 (string-append "rust-" (string-join (string-split name #\_) "-")))
256
257 \f
258 ;;;
259 ;;; Updater
260 ;;;
261
262 (define (crate-package? package)
263 "Return true if PACKAGE is a Rust crate from crates.io."
264 (let ((source-url (and=> (package-source package) origin-uri))
265 (fetch-method (and=> (package-source package) origin-method)))
266 (and (eq? fetch-method download:url-fetch)
267 (match source-url
268 ((? string?)
269 (crate-url? source-url))
270 ((source-url ...)
271 (any crate-url? source-url))))))
272
273 (define (latest-release package)
274 "Return an <upstream-source> for the latest release of PACKAGE."
275 (let* ((crate-name (guix-package->crate-name package))
276 (crate (lookup-crate crate-name))
277 (version (crate-latest-version crate))
278 (url (crate-uri crate-name version)))
279 (upstream-source
280 (package (package-name package))
281 (version version)
282 (urls (list url)))))
283
284 (define %crate-updater
285 (upstream-updater
286 (name 'crates)
287 (description "Updater for crates.io packages")
288 (pred crate-package?)
289 (latest latest-release)))
290