1 ;;; Copyright © 2018 Julien Lepiller <julien@lepiller.eu>
3 ;;; This file is part of GNU Guix.
5 ;;; GNU Guix is free software; you can redistribute it and/or modify it
6 ;;; under the terms of the GNU General Public License as published by
7 ;;; the Free Software Foundation; either version 3 of the License, or (at
8 ;;; your option) any later version.
10 ;;; GNU Guix is distributed in the hope that it will be useful, but
11 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
12 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ;;; GNU General Public License for more details.
15 ;;; You should have received a copy of the GNU General Public License
16 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18 (define-module (guix import opam)
19 #:use-module (ice-9 ftw)
20 #:use-module (ice-9 match)
21 #:use-module (ice-9 peg)
22 #:use-module (ice-9 receive)
23 #:use-module ((ice-9 rdelim) #:select (read-line))
24 #:use-module (ice-9 textual-ports)
25 #:use-module (ice-9 vlist)
26 #:use-module (srfi srfi-1)
27 #:use-module (srfi srfi-2)
28 #:use-module (web uri)
29 #:use-module (guix build-system)
30 #:use-module (guix build-system ocaml)
31 #:use-module (guix http-client)
32 #:use-module (guix git)
33 #:use-module (guix ui)
34 #:use-module (guix packages)
35 #:use-module (guix upstream)
36 #:use-module (guix utils)
37 #:use-module (guix import utils)
38 #:use-module ((guix licenses) #:prefix license:)
39 #:export (opam->guix-package
43 ;; Define a PEG parser for the opam format
44 (define-peg-pattern comment none (and "#" (* STRCHR) "\n"))
45 (define-peg-pattern SP none (or " " "\n" comment))
46 (define-peg-pattern SP2 body (or " " "\n"))
47 (define-peg-pattern QUOTE none "\"")
48 (define-peg-pattern QUOTE2 body "\"")
49 (define-peg-pattern COLON none ":")
50 ;; A string character is any character that is not a quote, or a quote preceded by a backslash.
51 (define-peg-pattern STRCHR body
52 (or " " "!" (and (ignore "\\") "\"")
53 (and (ignore "\\") "\\") (range #\# #\頋)))
54 (define-peg-pattern operator all (or "=" "!" "<" ">"))
56 (define-peg-pattern records body (* (and (or record weird-record) (* SP))))
57 (define-peg-pattern record all (and key COLON (* SP) value))
58 (define-peg-pattern weird-record all (and key (* SP) dict))
59 (define-peg-pattern key body (+ (or (range #\a #\z) "-")))
60 (define-peg-pattern value body (and (or conditional-value ground-value operator) (* SP)))
61 (define-peg-pattern choice-pat all (and (ignore "(") (* SP) choice (* SP) (ignore ")")))
62 (define-peg-pattern choice body
63 (or (and (or conditional-value ground-value) (* SP) (ignore "|") (* SP) choice)
66 (define-peg-pattern ground-value body (and (or multiline-string string-pat choice-pat list-pat var) (* SP)))
67 (define-peg-pattern conditional-value all (and ground-value (* SP) condition))
68 (define-peg-pattern string-pat all (and QUOTE (* STRCHR) QUOTE))
69 (define-peg-pattern list-pat all (and (ignore "[") (* SP) (* (and value (* SP))) (ignore "]")))
70 (define-peg-pattern var all (+ (or (range #\a #\z) "-")))
71 (define-peg-pattern multiline-string all
72 (and QUOTE QUOTE QUOTE (* SP)
73 (* (or SP2 STRCHR (and QUOTE2 (not-followed-by QUOTE))
74 (and QUOTE2 QUOTE2 (not-followed-by QUOTE))))
76 (define-peg-pattern dict all (and (ignore "{") (* SP) records (* SP) (ignore "}")))
78 (define-peg-pattern condition body (and (ignore "{") condition-form (ignore "}")))
80 (define-peg-pattern condition-form body
83 (or condition-and condition-or condition-form2)
85 (define-peg-pattern condition-form2 body
86 (and (* SP) (or condition-greater-or-equal condition-greater
87 condition-lower-or-equal condition-lower
88 condition-neq condition-eq condition-not
89 condition-content) (* SP)))
91 ;(define-peg-pattern condition-operator all (and (ignore operator) (* SP) condition-string))
92 (define-peg-pattern condition-greater-or-equal all (and (ignore (and ">" "=")) (* SP) condition-string))
93 (define-peg-pattern condition-greater all (and (ignore ">") (* SP) condition-string))
94 (define-peg-pattern condition-lower-or-equal all (and (ignore (and "<" "=")) (* SP) condition-string))
95 (define-peg-pattern condition-lower all (and (ignore "<") (* SP) condition-string))
96 (define-peg-pattern condition-and all (and condition-form2 (* SP) (? (ignore "&")) (* SP) condition-form))
97 (define-peg-pattern condition-or all (and condition-form2 (* SP) (ignore "|") (* SP) condition-form))
98 (define-peg-pattern condition-eq all (and (? condition-content) (* SP) (ignore "=") (* SP) condition-content))
99 (define-peg-pattern condition-neq all (and (? condition-content) (* SP) (ignore (and "!" "=")) (* SP) condition-content))
100 (define-peg-pattern condition-not all (and (ignore (and "!")) (* SP) condition-content))
101 (define-peg-pattern condition-content body (or condition-paren condition-string condition-var))
102 (define-peg-pattern condition-content2 body (and condition-content (* SP) (not-followed-by (or "&" "=" "!"))))
103 (define-peg-pattern condition-paren body (and "(" condition-form ")"))
104 (define-peg-pattern condition-string all (and QUOTE (* STRCHR) QUOTE))
105 (define-peg-pattern condition-var all (+ (or (range #\a #\z) "-" ":")))
107 (define (get-opam-repository)
108 "Update or fetch the latest version of the opam repository and return the
109 path to the repository."
110 (receive (location commit)
111 (update-cached-checkout "https://github.com/ocaml/opam-repository")
114 (define (latest-version versions)
115 "Find the most recent version from a list of versions."
116 (fold (lambda (a b) (if (version>? a b) a b)) (car versions) versions))
118 (define (find-latest-version package repository)
119 "Get the latest version of a package as described in the given repository."
120 (let* ((dir (string-append repository "/packages/" package))
121 (versions (scandir dir (lambda (name) (not (string-prefix? "." name))))))
125 (string-join (cdr (string-split dir #\.)) "."))
127 ;; Workaround for janestreet re-versionning
128 (let ((v-versions (filter (lambda (version) (string-prefix? "v" version)) versions)))
129 (if (null? v-versions)
130 (latest-version versions)
131 (string-append "v" (latest-version (map (lambda (version) (substring version 1)) v-versions))))))
133 (format #t (G_ "Package not found in opam repository: ~a~%") package)
136 (define (get-metadata opam-file)
137 (with-input-from-file opam-file
139 (peg:tree (match-pattern records (get-string-all (current-input-port)))))))
141 (define (substitute-char str what with)
142 (string-join (string-split str what) with))
144 (define (ocaml-name->guix-name name)
147 ((equal? name "ocamlfind") "ocaml-findlib")
148 ((string-prefix? "ocaml" name) name)
149 ((string-prefix? "conf-" name) (substring name 5))
150 (else (string-append "ocaml-" name)))
153 (define (metadata-ref file lookup)
154 (fold (lambda (record acc)
157 (if (equal? key lookup)
159 (('list-pat . stuff) stuff)
160 (('string-pat stuff) stuff)
161 (('multiline-string stuff) stuff)
162 (('dict records ...) records))
166 (define (native? condition)
168 (('condition-var var)
174 ((or ('condition-or cond-left cond-right) ('condition-and cond-left cond-right))
175 (or (native? cond-left)
176 (native? cond-right)))
179 (define (dependency->input dependency)
181 (('string-pat str) str)
182 ;; Arbitrary select the first dependency
183 (('choice-pat choice ...) (dependency->input (car choice)))
184 (('conditional-value val condition)
185 (if (native? condition) "" (dependency->input val)))))
187 (define (dependency->native-input dependency)
189 (('string-pat str) "")
190 ;; Arbitrary select the first dependency
191 (('choice-pat choice ...) (dependency->input (car choice)))
192 (('conditional-value val condition)
193 (if (native? condition) (dependency->input val) ""))))
195 (define (dependency->name dependency)
197 (('string-pat str) str)
198 ;; Arbitrary select the first dependency
199 (('choice-pat choice ...) (dependency->input (car choice)))
200 (('conditional-value val condition)
201 (dependency->name val))))
203 (define (dependency-list->names lst)
207 (string-prefix? "conf-" name)
208 (equal? name "ocaml")
209 (equal? name "findlib"))))
210 (map dependency->name lst)))
212 (define (ocaml-names->guix-names names)
213 (map ocaml-name->guix-name
214 (remove (lambda (name)
215 (or (equal? "" name))
216 (equal? "ocaml" name))
219 (define (depends->inputs depends)
220 (filter (lambda (name)
221 (and (not (equal? "" name))
222 (not (equal? "ocaml" name))
223 (not (equal? "ocamlfind" name))))
224 (map dependency->input depends)))
226 (define (depends->native-inputs depends)
227 (filter (lambda (name) (not (equal? "" name)))
228 (map dependency->native-input depends)))
230 (define (dependency-list->inputs lst)
233 (list dependency (list 'unquote (string->symbol dependency))))
234 (ocaml-names->guix-names lst)))
236 (define (opam-fetch name)
237 (and-let* ((repository (get-opam-repository))
238 (version (find-latest-version name repository))
239 (file (string-append repository "/packages/" name "/" name "." version "/opam")))
240 `(("metadata" ,@(get-metadata file))
241 ("version" . ,version))))
243 (define (opam->guix-package name)
244 (and-let* ((opam-file (opam-fetch name))
245 (version (assoc-ref opam-file "version"))
246 (opam-content (assoc-ref opam-file "metadata"))
247 (url-dict (metadata-ref opam-content "url"))
248 (source-url (metadata-ref url-dict "src"))
249 (requirements (metadata-ref opam-content "depends"))
250 (dependencies (filter
252 (not (member name '("dune" "jbuilder"))))
253 (dependency-list->names requirements)))
254 (native-dependencies (depends->native-inputs requirements))
255 (inputs (dependency-list->inputs (depends->inputs requirements)))
256 (native-inputs (dependency-list->inputs
257 ;; Do not add dune nor jbuilder since they are
258 ;; implicit inputs of the dune-build-system.
261 (not (member name '("dune" "jbuilder"))))
262 native-dependencies))))
263 ;; If one of these are required at build time, it means we
264 ;; can use the much nicer dune-build-system.
265 (let ((use-dune? (or (member "dune" native-dependencies)
266 (member "jbuilder" native-dependencies))))
267 (call-with-temporary-output-file
269 (and (url-fetch source-url temp)
272 (name ,(ocaml-name->guix-name name))
273 (version ,(if (string-prefix? "v" version)
274 (substring version 1)
280 (sha256 (base32 ,(guix-hash-url temp)))))
281 (build-system ,(if use-dune?
283 'ocaml-build-system))
286 `((inputs ,(list 'quasiquote inputs))))
287 ,@(if (null? native-inputs)
289 `((native-inputs ,(list 'quasiquote native-inputs))))
290 ,@(if (equal? name (guix-name->opam-name (ocaml-name->guix-name name)))
293 ,(list 'quasiquote `((upstream-name . ,name))))))
294 (home-page ,(metadata-ref opam-content "homepage"))
295 (synopsis ,(metadata-ref opam-content "synopsis"))
296 (description ,(metadata-ref opam-content "description"))
300 (define (opam-recursive-import package-name)
301 (recursive-import package-name #f
302 #:repo->guix-package (lambda (name repo)
303 (opam->guix-package name))
304 #:guix-name ocaml-name->guix-name))
306 (define (guix-name->opam-name name)
307 (if (string-prefix? "ocaml-" name)
311 (define (guix-package->opam-name package)
312 "Given an OCaml PACKAGE built from OPAM, return the name of the
314 (let ((upstream-name (assoc-ref
315 (package-properties package)
317 (name (package-name package)))
320 (guix-name->opam-name name))))
322 (define (opam-package? package)
323 "Return true if PACKAGE is an OCaml package from OPAM"
325 (member (build-system-name (package-build-system package)) '(dune ocaml))
326 (not (string-prefix? "ocaml4" (package-name package)))))
328 (define (latest-release package)
329 "Return an <upstream-source> for the latest release of PACKAGE."
330 (and-let* ((opam-name (guix-package->opam-name package))
331 (opam-file (opam-fetch opam-name))
332 (version (assoc-ref opam-file "version"))
333 (opam-content (assoc-ref opam-file "metadata"))
334 (url-dict (metadata-ref opam-content "url"))
335 (source-url (metadata-ref url-dict "src")))
337 (package (package-name package))
339 (urls (list source-url)))))
341 (define %opam-updater
344 (description "Updater for OPAM packages")
346 (latest latest-release)))