opam.scm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2018 Julien Lepiller <julien@lepiller.eu>
  3. ;;; Copyright © 2020 Martin Becze <mjbecze@riseup.net>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix import opam)
  20. #:use-module (ice-9 ftw)
  21. #:use-module (ice-9 match)
  22. #:use-module (ice-9 peg)
  23. #:use-module (ice-9 receive)
  24. #:use-module ((ice-9 rdelim) #:select (read-line))
  25. #:use-module (ice-9 textual-ports)
  26. #:use-module (ice-9 vlist)
  27. #:use-module (srfi srfi-1)
  28. #:use-module (srfi srfi-2)
  29. #:use-module (web uri)
  30. #:use-module (guix build-system)
  31. #:use-module (guix build-system ocaml)
  32. #:use-module (guix http-client)
  33. #:use-module (guix git)
  34. #:use-module (guix ui)
  35. #:use-module (guix packages)
  36. #:use-module (guix upstream)
  37. #:use-module (guix utils)
  38. #:use-module (guix import utils)
  39. #:use-module ((guix licenses) #:prefix license:)
  40. #:export (opam->guix-package
  41. opam-recursive-import
  42. %opam-updater
  43. ;; The following patterns are exported for testing purposes.
  44. string-pat
  45. multiline-string
  46. list-pat
  47. dict
  48. condition))
  49. ;; Define a PEG parser for the opam format
  50. (define-peg-pattern comment none (and "#" (* COMMCHR) "\n"))
  51. (define-peg-pattern SP none (or " " "\n" comment))
  52. (define-peg-pattern SP2 body (or " " "\n"))
  53. (define-peg-pattern QUOTE none "\"")
  54. (define-peg-pattern QUOTE2 body "\"")
  55. (define-peg-pattern COLON none ":")
  56. ;; A string character is any character that is not a quote, or a quote preceded by a backslash.
  57. (define-peg-pattern COMMCHR none
  58. (or " " "!" "\\" "\"" (range #\# #\頋)))
  59. (define-peg-pattern STRCHR body
  60. (or " " "!" "\n" (and (ignore "\\") "\"")
  61. (ignore "\\\n") (and (ignore "\\") "\\")
  62. (range #\# #\頋)))
  63. (define-peg-pattern operator all (or "=" "!" "<" ">"))
  64. (define-peg-pattern records body (* (and (or record weird-record) (* SP))))
  65. (define-peg-pattern record all (and key COLON (* SP) value))
  66. (define-peg-pattern weird-record all (and key (* SP) dict))
  67. (define-peg-pattern key body (+ (or (range #\a #\z) "-")))
  68. (define-peg-pattern value body (and (or conditional-value ground-value operator) (* SP)))
  69. (define-peg-pattern choice-pat all (and (ignore "(") (* SP) choice (* SP) (ignore ")")))
  70. (define-peg-pattern choice body
  71. (or (and (or conditional-value ground-value) (* SP) (ignore "|") (* SP) choice)
  72. group-pat
  73. conditional-value
  74. ground-value))
  75. (define-peg-pattern group-pat all
  76. (and (or conditional-value ground-value) (* SP) (ignore "&") (* SP)
  77. (or group-pat conditional-value ground-value)))
  78. (define-peg-pattern ground-value body (and (or multiline-string string-pat choice-pat list-pat var) (* SP)))
  79. (define-peg-pattern conditional-value all (and ground-value (* SP) condition))
  80. (define-peg-pattern string-pat all (and QUOTE (* STRCHR) QUOTE))
  81. (define-peg-pattern list-pat all (and (ignore "[") (* SP) (* (and value (* SP))) (ignore "]")))
  82. (define-peg-pattern var all (+ (or (range #\a #\z) "-")))
  83. (define-peg-pattern multiline-string all
  84. (and QUOTE QUOTE QUOTE (* SP)
  85. (* (or SP2 STRCHR (and QUOTE2 (not-followed-by QUOTE))
  86. (and QUOTE2 QUOTE2 (not-followed-by QUOTE))))
  87. QUOTE QUOTE QUOTE))
  88. (define-peg-pattern dict all (and (ignore "{") (* SP) records (* SP) (ignore "}")))
  89. (define-peg-pattern condition body (and (ignore "{") condition-form (ignore "}")))
  90. (define-peg-pattern condition-form body
  91. (and
  92. (* SP)
  93. (or condition-and condition-or condition-form2)
  94. (* SP)))
  95. (define-peg-pattern condition-form2 body
  96. (and (* SP) (or condition-greater-or-equal condition-greater
  97. condition-lower-or-equal condition-lower
  98. condition-neq condition-eq condition-not
  99. condition-content) (* SP)))
  100. ;(define-peg-pattern condition-operator all (and (ignore operator) (* SP) condition-string))
  101. (define-peg-pattern condition-greater-or-equal all (and (ignore (and ">" "=")) (* SP) condition-string))
  102. (define-peg-pattern condition-greater all (and (ignore ">") (* SP) condition-string))
  103. (define-peg-pattern condition-lower-or-equal all (and (ignore (and "<" "=")) (* SP) condition-string))
  104. (define-peg-pattern condition-lower all (and (ignore "<") (* SP) condition-string))
  105. (define-peg-pattern condition-and all (and condition-form2 (* SP) (? (ignore "&")) (* SP) condition-form))
  106. (define-peg-pattern condition-or all (and condition-form2 (* SP) (ignore "|") (* SP) condition-form))
  107. (define-peg-pattern condition-eq all (and (? condition-content) (* SP) (ignore "=") (* SP) condition-content))
  108. (define-peg-pattern condition-neq all (and (? condition-content) (* SP) (ignore (and "!" "=")) (* SP) condition-content))
  109. (define-peg-pattern condition-not all (and (ignore (and "!")) (* SP) condition-content))
  110. (define-peg-pattern condition-content body (or condition-paren condition-string condition-var))
  111. (define-peg-pattern condition-content2 body (and condition-content (* SP) (not-followed-by (or "&" "=" "!"))))
  112. (define-peg-pattern condition-paren body (and "(" condition-form ")"))
  113. (define-peg-pattern condition-string all (and QUOTE (* STRCHR) QUOTE))
  114. (define-peg-pattern condition-var all (+ (or (range #\a #\z) "-" ":")))
  115. (define* (get-opam-repository #:optional repo)
  116. "Update or fetch the latest version of the opam repository and return the
  117. path to the repository."
  118. (let ((url (cond
  119. ((or (not repo) (equal? repo 'opam))
  120. "https://github.com/ocaml/opam-repository")
  121. ((string-prefix? "coq-" (symbol->string repo))
  122. "https://github.com/coq/opam-coq-archive")
  123. ((equal? repo 'coq) "https://github.com/coq/opam-coq-archive")
  124. (else (throw 'unknown-repository repo)))))
  125. (receive (location commit _)
  126. (update-cached-checkout url)
  127. (cond
  128. ((or (not repo) (equal? repo 'opam))
  129. location)
  130. ((equal? repo 'coq)
  131. (string-append location "/released"))
  132. ((string-prefix? "coq-" (symbol->string repo))
  133. (string-append location "/" (substring (symbol->string repo) 4)))
  134. (else location)))))
  135. ;; Prevent Guile 3 from inlining this procedure so we can mock it in tests.
  136. (set! get-opam-repository get-opam-repository)
  137. (define (latest-version versions)
  138. "Find the most recent version from a list of versions."
  139. (fold (lambda (a b) (if (version>? a b) a b)) (car versions) versions))
  140. (define (find-latest-version package repository)
  141. "Get the latest version of a package as described in the given repository."
  142. (let* ((dir (string-append repository "/packages/" package))
  143. (versions (scandir dir (lambda (name) (not (string-prefix? "." name))))))
  144. (if versions
  145. (let ((versions (map
  146. (lambda (dir)
  147. (string-join (cdr (string-split dir #\.)) "."))
  148. versions)))
  149. ;; Workaround for janestreet re-versionning
  150. (let ((v-versions (filter (lambda (version) (string-prefix? "v" version)) versions)))
  151. (if (null? v-versions)
  152. (latest-version versions)
  153. (string-append "v" (latest-version (map (lambda (version) (substring version 1)) v-versions))))))
  154. (begin
  155. (format #t (G_ "Package not found in opam repository: ~a~%") package)
  156. #f))))
  157. (define (get-metadata opam-file)
  158. (with-input-from-file opam-file
  159. (lambda _
  160. (peg:tree (match-pattern records (get-string-all (current-input-port)))))))
  161. (define (substitute-char str what with)
  162. (string-join (string-split str what) with))
  163. (define (ocaml-name->guix-name name)
  164. (substitute-char
  165. (cond
  166. ((equal? name "ocamlfind") "ocaml-findlib")
  167. ((equal? name "coq") name)
  168. ((string-prefix? "ocaml" name) name)
  169. ((string-prefix? "conf-" name) (substring name 5))
  170. (else (string-append "ocaml-" name)))
  171. #\_ "-"))
  172. (define (metadata-ref file lookup)
  173. (fold (lambda (record acc)
  174. (match record
  175. ((record key val)
  176. (if (equal? key lookup)
  177. (match val
  178. (('list-pat . stuff) stuff)
  179. (('string-pat stuff) stuff)
  180. (('multiline-string stuff) stuff)
  181. (('dict records ...) records))
  182. acc))))
  183. #f file))
  184. (define (native? condition)
  185. (match condition
  186. (('condition-var var)
  187. (match var
  188. ("with-test" #t)
  189. ("test" #t)
  190. ("build" #t)
  191. (_ #f)))
  192. ((or ('condition-or cond-left cond-right) ('condition-and cond-left cond-right))
  193. (or (native? cond-left)
  194. (native? cond-right)))
  195. (_ #f)))
  196. (define (dependency->input dependency)
  197. (match dependency
  198. (('string-pat str) str)
  199. ;; Arbitrary select the first dependency
  200. (('choice-pat choice ...) (dependency->input (car choice)))
  201. (('group-pat val ...) (map dependency->input val))
  202. (('conditional-value val condition)
  203. (if (native? condition) "" (dependency->input val)))))
  204. (define (dependency->native-input dependency)
  205. (match dependency
  206. (('string-pat str) "")
  207. ;; Arbitrary select the first dependency
  208. (('choice-pat choice ...) (dependency->native-input (car choice)))
  209. (('group-pat val ...) (map dependency->native-input val))
  210. (('conditional-value val condition)
  211. (if (native? condition) (dependency->input val) ""))))
  212. (define (dependency->name dependency)
  213. (match dependency
  214. (('string-pat str) str)
  215. ;; Arbitrary select the first dependency
  216. (('choice-pat choice ...) (dependency->name (car choice)))
  217. (('group-pat val ...) (map dependency->name val))
  218. (('conditional-value val condition)
  219. (dependency->name val))))
  220. (define (dependency-list->names lst)
  221. (filter
  222. (lambda (name)
  223. (not (or
  224. (string-prefix? "conf-" name)
  225. (equal? name "ocaml")
  226. (equal? name "findlib"))))
  227. (map dependency->name lst)))
  228. (define (ocaml-names->guix-names names)
  229. (map ocaml-name->guix-name
  230. (remove (lambda (name)
  231. (or (equal? "" name))
  232. (equal? "ocaml" name))
  233. names)))
  234. (define (filter-dependencies depends)
  235. "Remove implicit dependencies from the list of dependencies in @var{depends}."
  236. (filter (lambda (name)
  237. (and (not (member name '("" "ocaml" "ocamlfind" "dune" "jbuilder")))
  238. (not (string-prefix? "base-" name))))
  239. depends))
  240. (define (depends->inputs depends)
  241. (filter-dependencies (map dependency->input depends)))
  242. (define (depends->native-inputs depends)
  243. (filter (lambda (name) (not (equal? "" name)))
  244. (map dependency->native-input depends)))
  245. (define (dependency-list->inputs lst)
  246. (map
  247. (lambda (dependency)
  248. (list dependency (list 'unquote (string->symbol dependency))))
  249. (ocaml-names->guix-names lst)))
  250. (define* (opam-fetch name #:optional (repository (get-opam-repository)))
  251. (and-let* ((repository repository)
  252. (version (find-latest-version name repository))
  253. (file (string-append repository "/packages/" name "/" name "." version "/opam")))
  254. `(("metadata" ,@(get-metadata file))
  255. ("version" . ,(if (string-prefix? "v" version)
  256. (substring version 1)
  257. version)))))
  258. (define* (opam->guix-package name #:key (repo 'opam) version)
  259. "Import OPAM package NAME from REPOSITORY (a directory name) or, if
  260. REPOSITORY is #f, from the official OPAM repository. Return a 'package' sexp
  261. or #f on failure."
  262. (and-let* ((opam-file (opam-fetch name (get-opam-repository repo)))
  263. (version (assoc-ref opam-file "version"))
  264. (opam-content (assoc-ref opam-file "metadata"))
  265. (url-dict (metadata-ref opam-content "url"))
  266. (source-url (or (metadata-ref url-dict "src")
  267. (metadata-ref url-dict "archive")))
  268. (requirements (metadata-ref opam-content "depends"))
  269. (names (dependency-list->names requirements))
  270. (dependencies (filter-dependencies names))
  271. (native-dependencies (depends->native-inputs requirements))
  272. (inputs (dependency-list->inputs (depends->inputs requirements)))
  273. (native-inputs (dependency-list->inputs
  274. ;; Do not add dune nor jbuilder since they are
  275. ;; implicit inputs of the dune-build-system.
  276. (filter
  277. (lambda (name)
  278. (not (member name '("dune" "jbuilder"))))
  279. native-dependencies))))
  280. (let ((use-dune? (member "dune" names)))
  281. (call-with-temporary-output-file
  282. (lambda (temp port)
  283. (and (url-fetch source-url temp)
  284. (values
  285. `(package
  286. (name ,(ocaml-name->guix-name name))
  287. (version ,(if (string-prefix? "v" version)
  288. (substring version 1)
  289. version))
  290. (source
  291. (origin
  292. (method url-fetch)
  293. (uri ,source-url)
  294. (sha256 (base32 ,(guix-hash-url temp)))))
  295. (build-system ,(if use-dune?
  296. 'dune-build-system
  297. 'ocaml-build-system))
  298. ,@(if (null? inputs)
  299. '()
  300. `((propagated-inputs ,(list 'quasiquote inputs))))
  301. ,@(if (null? native-inputs)
  302. '()
  303. `((native-inputs ,(list 'quasiquote native-inputs))))
  304. ,@(if (equal? name (guix-name->opam-name (ocaml-name->guix-name name)))
  305. '()
  306. `((properties
  307. ,(list 'quasiquote `((upstream-name . ,name))))))
  308. (home-page ,(metadata-ref opam-content "homepage"))
  309. (synopsis ,(metadata-ref opam-content "synopsis"))
  310. (description ,(metadata-ref opam-content "description"))
  311. (license #f))
  312. (filter
  313. (lambda (name)
  314. (not (member name '("dune" "jbuilder"))))
  315. dependencies))))))))
  316. (define* (opam-recursive-import package-name #:key repo)
  317. (recursive-import package-name
  318. #:repo->guix-package opam->guix-package
  319. #:guix-name ocaml-name->guix-name
  320. #:repo repo))
  321. (define (guix-name->opam-name name)
  322. (if (string-prefix? "ocaml-" name)
  323. (substring name 6)
  324. name))
  325. (define (guix-package->opam-name package)
  326. "Given an OCaml PACKAGE built from OPAM, return the name of the
  327. package in OPAM."
  328. (let ((upstream-name (assoc-ref
  329. (package-properties package)
  330. 'upstream-name))
  331. (name (package-name package)))
  332. (if upstream-name
  333. upstream-name
  334. (guix-name->opam-name name))))
  335. (define (opam-package? package)
  336. "Return true if PACKAGE is an OCaml package from OPAM"
  337. (and
  338. (member (build-system-name (package-build-system package)) '(dune ocaml))
  339. (not (string-prefix? "ocaml4" (package-name package)))))
  340. (define (latest-release package)
  341. "Return an <upstream-source> for the latest release of PACKAGE."
  342. (and-let* ((opam-name (guix-package->opam-name package))
  343. (opam-file (opam-fetch opam-name))
  344. (version (assoc-ref opam-file "version"))
  345. (opam-content (assoc-ref opam-file "metadata"))
  346. (url-dict (metadata-ref opam-content "url"))
  347. (source-url (metadata-ref url-dict "src")))
  348. (upstream-source
  349. (package (package-name package))
  350. (version version)
  351. (urls (list source-url)))))
  352. (define %opam-updater
  353. (upstream-updater
  354. (name 'opam)
  355. (description "Updater for OPAM packages")
  356. (pred opam-package?)
  357. (latest latest-release)))