opam.scm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
  5. ;;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev>
  6. ;;; Copyright © 2021, 2022 Alice Brenon <alice.brenon@ens-lyon.fr>
  7. ;;; Copyright © 2022 Hartmut Goebel <h.goebel@crazy-compilers.com>
  8. ;;;
  9. ;;; This file is part of GNU Guix.
  10. ;;;
  11. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  12. ;;; under the terms of the GNU General Public License as published by
  13. ;;; the Free Software Foundation; either version 3 of the License, or (at
  14. ;;; your option) any later version.
  15. ;;;
  16. ;;; GNU Guix is distributed in the hope that it will be useful, but
  17. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  18. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. ;;; GNU General Public License for more details.
  20. ;;;
  21. ;;; You should have received a copy of the GNU General Public License
  22. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  23. (define-module (guix import opam)
  24. #:use-module (ice-9 match)
  25. #:use-module (ice-9 peg)
  26. #:use-module ((ice-9 popen) #:select (open-pipe*))
  27. #:use-module (ice-9 textual-ports)
  28. #:use-module (srfi srfi-1)
  29. #:use-module (srfi srfi-2)
  30. #:use-module ((srfi srfi-26) #:select (cut))
  31. #:use-module (srfi srfi-34)
  32. #:use-module ((web uri) #:select (string->uri uri->string))
  33. #:use-module ((guix build utils) #:select (dump-port find-files mkdir-p))
  34. #:use-module (guix build-system)
  35. #:use-module (guix i18n)
  36. #:use-module (guix diagnostics)
  37. #:use-module (guix http-client)
  38. #:use-module (guix packages)
  39. #:use-module (guix upstream)
  40. #:use-module ((guix utils) #:select (cache-directory
  41. version>?
  42. call-with-temporary-output-file))
  43. #:use-module ((guix import utils) #:select (beautify-description
  44. guix-hash-url
  45. recursive-import
  46. spdx-string->license
  47. url-fetch))
  48. #:export (opam->guix-package
  49. opam-recursive-import
  50. %opam-updater
  51. ;; The following patterns are exported for testing purposes.
  52. string-pat
  53. multiline-string
  54. list-pat
  55. dict
  56. condition))
  57. ;; Define a PEG parser for the opam format
  58. (define-peg-pattern comment none (and "#" (* COMMCHR) "\n"))
  59. (define-peg-pattern SP none (or " " "\n" "\t" comment))
  60. (define-peg-pattern SP2 body (or " " "\n" "\t"))
  61. (define-peg-pattern QUOTE none "\"")
  62. (define-peg-pattern QUOTE2 body "\"")
  63. (define-peg-pattern COLON none ":")
  64. ;; A string character is any character that is not a quote, or a quote preceded by a backslash.
  65. (define-peg-pattern COMMCHR none
  66. (or " " "!" "\\" "\"" (range #\# #\頋)))
  67. (define-peg-pattern STRCHR body
  68. (or " " "!" "\n" (and (ignore "\\") "\"")
  69. (ignore "\\\n") (and (ignore "\\") "\\")
  70. (range #\# #\頋)))
  71. (define-peg-pattern operator all (or "=" "!" "<" ">"))
  72. (define-peg-pattern records body (and (* SP) (* (and (or record weird-record) (* SP)))))
  73. (define-peg-pattern record all (and key COLON (* SP) value))
  74. (define-peg-pattern weird-record all (and key (* SP) dict))
  75. (define-peg-pattern key body (+ (or (range #\a #\z) "-")))
  76. (define-peg-pattern value body (and (or conditional-value ground-value operator) (* SP)))
  77. (define-peg-pattern choice-pat all (and (ignore "(") (* SP) choice (* SP) (ignore ")")))
  78. (define-peg-pattern choice body
  79. (or (and (or conditional-value ground-value) (* SP) (ignore "|") (* SP) choice)
  80. group-pat
  81. conditional-value
  82. ground-value))
  83. (define-peg-pattern group-pat all
  84. (and (or conditional-value ground-value) (* SP) (ignore "&") (* SP)
  85. (or group-pat conditional-value ground-value)))
  86. (define-peg-pattern ground-value body (and (or multiline-string string-pat choice-pat list-pat var) (* SP)))
  87. (define-peg-pattern conditional-value all (and ground-value (* SP) condition))
  88. (define-peg-pattern string-pat all (and QUOTE (* STRCHR) QUOTE))
  89. (define-peg-pattern list-pat all (and (ignore "[") (* SP) (* (and value (* SP))) (ignore "]")))
  90. (define-peg-pattern var all (+ (or (range #\a #\z) "-")))
  91. (define-peg-pattern multiline-string all
  92. (and QUOTE QUOTE QUOTE (* SP)
  93. (* (or SP2 STRCHR (and QUOTE2 (not-followed-by QUOTE))
  94. (and QUOTE2 QUOTE2 (not-followed-by QUOTE))))
  95. QUOTE QUOTE QUOTE))
  96. (define-peg-pattern dict all (and (ignore "{") (* SP) records (* SP) (ignore "}")))
  97. (define-peg-pattern condition body (and (ignore "{") condition-form (ignore "}")))
  98. (define-peg-pattern condition-form body
  99. (and
  100. (* SP)
  101. (or condition-and condition-or condition-form2)
  102. (* SP)))
  103. (define-peg-pattern condition-form2 body
  104. (and (* SP) (or condition-greater-or-equal condition-greater
  105. condition-lower-or-equal condition-lower
  106. condition-neq condition-eq condition-not
  107. condition-content) (* SP)))
  108. ;(define-peg-pattern condition-operator all (and (ignore operator) (* SP) condition-string))
  109. (define-peg-pattern condition-greater-or-equal all (and (ignore (and ">" "=")) (* SP) condition-string))
  110. (define-peg-pattern condition-greater all (and (ignore ">") (* SP) condition-string))
  111. (define-peg-pattern condition-lower-or-equal all (and (ignore (and "<" "=")) (* SP) condition-string))
  112. (define-peg-pattern condition-lower all (and (ignore "<") (* SP) condition-string))
  113. (define-peg-pattern condition-and all (and condition-form2 (* SP) (? (ignore "&")) (* SP) condition-form))
  114. (define-peg-pattern condition-or all (and condition-form2 (* SP) (ignore "|") (* SP) condition-form))
  115. (define-peg-pattern condition-eq all (and (? condition-content) (* SP) (ignore "=") (* SP) condition-content))
  116. (define-peg-pattern condition-neq all (and (? condition-content) (* SP) (ignore (and "!" "=")) (* SP) condition-content))
  117. (define-peg-pattern condition-not all (and (ignore (and "!")) (* SP) condition-content))
  118. (define-peg-pattern condition-content body (or condition-paren condition-string condition-var))
  119. (define-peg-pattern condition-content2 body (and condition-content (* SP) (not-followed-by (or "&" "=" "!"))))
  120. (define-peg-pattern condition-paren body (and "(" condition-form ")"))
  121. (define-peg-pattern condition-string all (and QUOTE (* STRCHR) QUOTE))
  122. (define-peg-pattern condition-var all (+ (or (range #\a #\z) "-" ":")))
  123. (define (opam-cache-directory path)
  124. (string-append (cache-directory) "/opam/" path))
  125. (define known-repositories
  126. '((opam . "https://opam.ocaml.org")
  127. (coq . "https://coq.inria.fr/opam/released")
  128. (coq-released . "https://coq.inria.fr/opam/released")
  129. (coq-core-dev . "https://coq.inria.fr/opam/core-dev")
  130. (coq-extra-dev . "https://coq.inria.fr/opam/extra-dev")
  131. (grew . "http://opam.grew.fr")))
  132. (define (get-uri repo-root)
  133. (let ((archive-file (string-append repo-root "/index.tar.gz")))
  134. (or (string->uri archive-file)
  135. (begin
  136. (warning (G_ "'~a' is not a valid URI~%") archive-file)
  137. 'bad-repo))))
  138. (define (repo-type repo)
  139. (match (assoc-ref known-repositories (string->symbol repo))
  140. (#f (if (file-exists? repo)
  141. `(local ,repo)
  142. `(remote ,(get-uri repo))))
  143. (url `(remote ,(get-uri url)))))
  144. (define (update-repository input)
  145. "Make sure the cache for opam repository INPUT is up-to-date"
  146. (let* ((output (opam-cache-directory (basename (port-filename input))))
  147. (cached-date (if (file-exists? output)
  148. (stat:mtime (stat output))
  149. (begin (mkdir-p output) 0))))
  150. (when (> (stat:mtime (stat input)) cached-date)
  151. (call-with-port
  152. (open-pipe* OPEN_WRITE "tar" "xz" "-C" output "-f" "-")
  153. (cut dump-port input <>)))
  154. output))
  155. (define* (get-opam-repository #:optional (repo "opam"))
  156. "Update or fetch the latest version of the opam repository and return the
  157. path to the repository."
  158. (match (repo-type repo)
  159. (('local p) p)
  160. (('remote 'bad-repo) #f) ; to weed it out during filter-map in opam-fetch
  161. (('remote r) (call-with-port (http-fetch/cached r) update-repository))))
  162. ;; Prevent Guile 3 from inlining this procedure so we can mock it in tests.
  163. (set! get-opam-repository get-opam-repository)
  164. (define (get-version-and-file path)
  165. "Analyse a candidate path and return an list containing information for proper
  166. version comparison as well as the source path for metadata."
  167. (and-let* ((metadata-file (string-append path "/opam"))
  168. (filename (basename path))
  169. (version (string-join (cdr (string-split filename #\.)) ".")))
  170. (and (file-exists? metadata-file)
  171. (eq? 'regular (stat:type (stat metadata-file)))
  172. (if (string-prefix? "v" version)
  173. `(V ,(substring version 1) ,metadata-file)
  174. `(digits ,version ,metadata-file)))))
  175. (define (keep-max-version a b)
  176. "Version comparison on the lists returned by the previous function taking the
  177. janestreet re-versioning into account (v-prefixed come first)."
  178. (match (cons a b)
  179. ((('V va _) . ('V vb _)) (if (version>? va vb) a b))
  180. ((('V _ _) . _) a)
  181. ((_ . ('V _ _)) b)
  182. ((('digits va _) . ('digits vb _)) (if (version>? va vb) a b))))
  183. (define (find-latest-version package repository)
  184. "Get the latest version of a package as described in the given repository."
  185. (let ((packages (string-append repository "/packages"))
  186. (filter (make-regexp (string-append "^" package "\\."))))
  187. (reduce keep-max-version #f
  188. (filter-map
  189. get-version-and-file
  190. (find-files packages filter #:directories? #t)))))
  191. (define (get-metadata opam-file)
  192. (with-input-from-file opam-file
  193. (lambda _
  194. (peg:tree (match-pattern records (get-string-all (current-input-port)))))))
  195. (define (substitute-char str what with)
  196. (string-join (string-split str what) with))
  197. (define (ocaml-name->guix-name name)
  198. (substitute-char
  199. (cond
  200. ((equal? name "ocamlfind") "ocaml-findlib")
  201. ((equal? name "coq") name)
  202. ((string-prefix? "ocaml" name) name)
  203. ((string-prefix? "conf-" name) (substring name 5))
  204. (else (string-append "ocaml-" name)))
  205. #\_ "-"))
  206. (define (metadata-ref file lookup)
  207. (fold (lambda (record acc)
  208. (match record
  209. ((record key val)
  210. (if (equal? key lookup)
  211. (match val
  212. (('list-pat . stuff) stuff)
  213. (('string-pat stuff) stuff)
  214. (('multiline-string stuff) stuff)
  215. (('dict records ...) records)
  216. (_ #f))
  217. acc))))
  218. #f file))
  219. (define (native? condition)
  220. (match condition
  221. (('condition-var var)
  222. (match var
  223. ("with-test" #t)
  224. ("test" #t)
  225. ("build" #t)
  226. (_ #f)))
  227. ((or ('condition-or cond-left cond-right) ('condition-and cond-left cond-right))
  228. (or (native? cond-left)
  229. (native? cond-right)))
  230. (_ #f)))
  231. (define (dependency->input dependency)
  232. (match dependency
  233. (('string-pat str) str)
  234. ;; Arbitrary select the first dependency
  235. (('choice-pat choice ...) (dependency->input (car choice)))
  236. (('group-pat val ...) (map dependency->input val))
  237. (('conditional-value val condition)
  238. (if (native? condition) "" (dependency->input val)))))
  239. (define (dependency->native-input dependency)
  240. (match dependency
  241. (('string-pat str) "")
  242. ;; Arbitrary select the first dependency
  243. (('choice-pat choice ...) (dependency->native-input (car choice)))
  244. (('group-pat val ...) (map dependency->native-input val))
  245. (('conditional-value val condition)
  246. (if (native? condition) (dependency->input val) ""))))
  247. (define (dependency->name dependency)
  248. (match dependency
  249. (('string-pat str) str)
  250. ;; Arbitrary select the first dependency
  251. (('choice-pat choice ...) (dependency->name (car choice)))
  252. (('group-pat val ...) (map dependency->name val))
  253. (('conditional-value val condition)
  254. (dependency->name val))))
  255. (define (dependency-list->names lst)
  256. (filter
  257. (lambda (name)
  258. (not (or
  259. (string-prefix? "conf-" name)
  260. (equal? name "ocaml")
  261. (equal? name "findlib"))))
  262. (map dependency->name lst)))
  263. (define (ocaml-names->guix-names names)
  264. (map ocaml-name->guix-name
  265. (remove (lambda (name)
  266. (or (equal? "" name))
  267. (equal? "ocaml" name))
  268. names)))
  269. (define (filter-dependencies depends)
  270. "Remove implicit dependencies from the list of dependencies in @var{depends}."
  271. (filter (lambda (name)
  272. (and (not (member name '("" "ocaml" "ocamlfind" "dune" "jbuilder")))
  273. (not (string-prefix? "base-" name))))
  274. depends))
  275. (define (depends->inputs depends)
  276. (filter-dependencies (map dependency->input depends)))
  277. (define (depends->native-inputs depends)
  278. (filter (lambda (name) (not (equal? "" name)))
  279. (map dependency->native-input depends)))
  280. (define (dependency-list->inputs lst)
  281. (map string->symbol
  282. (ocaml-names->guix-names lst)))
  283. (define* (opam-fetch name #:optional (repositories-specs '("opam")))
  284. (or (fold (lambda (repository others)
  285. (match (find-latest-version name repository)
  286. ((_ version file) `(("metadata" ,@(get-metadata file))
  287. ("version" . ,version)))
  288. (_ others)))
  289. #f
  290. (filter-map get-opam-repository repositories-specs))
  291. (warning (G_ "opam: package '~a' not found~%") name)))
  292. (define (opam->guix-source url-dict)
  293. (let ((source-url (and url-dict
  294. (or (metadata-ref url-dict "src")
  295. (metadata-ref url-dict "archive")))))
  296. (if source-url
  297. (call-with-temporary-output-file
  298. (lambda (temp port)
  299. (and (url-fetch source-url temp)
  300. `(origin
  301. (method url-fetch)
  302. (uri ,source-url)
  303. (sha256 (base32 ,(guix-hash-url temp)))))))
  304. 'no-source-information)))
  305. (define* (opam->guix-package name #:key (repo '("opam")) version #:allow-other-keys)
  306. "Import OPAM package NAME from REPO, a list of repository names, URLs, or
  307. file names. Return a 'package' sexp or #f on failure."
  308. (and-let* ((with-opam (if (member "opam" repo) repo (cons "opam" repo)))
  309. (opam-file (opam-fetch name with-opam))
  310. (version (assoc-ref opam-file "version"))
  311. (opam-content (assoc-ref opam-file "metadata"))
  312. (source (opam->guix-source (metadata-ref opam-content "url")))
  313. (requirements (metadata-ref opam-content "depends"))
  314. (names (dependency-list->names requirements))
  315. (dependencies (filter-dependencies names))
  316. (native-dependencies (depends->native-inputs requirements))
  317. (inputs (dependency-list->inputs (depends->inputs requirements)))
  318. (native-inputs (dependency-list->inputs
  319. ;; Do not add dune nor jbuilder since they are
  320. ;; implicit inputs of the dune-build-system.
  321. (filter
  322. (lambda (name)
  323. (not (member name '("dune" "jbuilder"))))
  324. native-dependencies))))
  325. (let ((use-dune? (member "dune" names)))
  326. (values
  327. `(package
  328. (name ,(ocaml-name->guix-name name))
  329. (version ,version)
  330. (source ,source)
  331. (build-system ,(if use-dune?
  332. 'dune-build-system
  333. 'ocaml-build-system))
  334. ,@(if (null? inputs)
  335. '()
  336. `((propagated-inputs (list ,@inputs))))
  337. ,@(if (null? native-inputs)
  338. '()
  339. `((native-inputs (list ,@native-inputs))))
  340. ,@(if (equal? name (guix-name->opam-name (ocaml-name->guix-name name)))
  341. '()
  342. `((properties
  343. ,(list 'quasiquote `((upstream-name . ,name))))))
  344. (home-page ,(metadata-ref opam-content "homepage"))
  345. (synopsis ,(metadata-ref opam-content "synopsis"))
  346. (description ,(and=> (metadata-ref opam-content "description")
  347. beautify-description))
  348. (license ,(spdx-string->license
  349. (metadata-ref opam-content "license"))))
  350. (filter
  351. (lambda (name)
  352. (not (member name '("dune" "jbuilder"))))
  353. dependencies)))))
  354. (define* (opam-recursive-import package-name #:key repo)
  355. (recursive-import package-name
  356. #:repo->guix-package opam->guix-package
  357. #:guix-name ocaml-name->guix-name
  358. #:repo repo))
  359. (define (guix-name->opam-name name)
  360. (if (string-prefix? "ocaml-" name)
  361. (substring name 6)
  362. name))
  363. (define (guix-package->opam-name package)
  364. "Given an OCaml PACKAGE built from OPAM, return the name of the
  365. package in OPAM."
  366. (let ((upstream-name (assoc-ref
  367. (package-properties package)
  368. 'upstream-name))
  369. (name (package-name package)))
  370. (if upstream-name
  371. upstream-name
  372. (guix-name->opam-name name))))
  373. (define (opam-package? package)
  374. "Return true if PACKAGE is an OCaml package from OPAM"
  375. (and
  376. (member (build-system-name (package-build-system package)) '(dune ocaml))
  377. (not (string-prefix? "ocaml4" (package-name package)))))
  378. (define* (latest-release package #:key (version #f))
  379. "Return an <upstream-source> for the latest release of PACKAGE."
  380. (when version
  381. (raise
  382. (formatted-message
  383. (G_ "~a updater doesn't support updating to a specific version, sorry.")
  384. "opam")))
  385. (and-let* ((opam-name (guix-package->opam-name package))
  386. (opam-file (opam-fetch opam-name))
  387. (version (assoc-ref opam-file "version"))
  388. (opam-content (assoc-ref opam-file "metadata"))
  389. (url-dict (metadata-ref opam-content "url"))
  390. (source-url (metadata-ref url-dict "src")))
  391. (upstream-source
  392. (package (package-name package))
  393. (version version)
  394. (urls (list source-url)))))
  395. (define %opam-updater
  396. (upstream-updater
  397. (name 'opam)
  398. (description "Updater for OPAM packages")
  399. (pred opam-package?)
  400. (import latest-release)))