cpan.scm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2014 Eric Bavier <bavier@member.fsf.org>
  3. ;;; Copyright © 2015 Mark H Weaver <mhw@netris.org>
  4. ;;; Copyright © 2016 Alex Sassmannshausen <alex@pompo.co>
  5. ;;; Copyright © 2017, 2018 Tobias Geerinckx-Rice <me@tobias.gr>
  6. ;;; Copyright © 2020, 2021 Ludovic Courtès <ludo@gnu.org>
  7. ;;;
  8. ;;; This file is part of GNU Guix.
  9. ;;;
  10. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  11. ;;; under the terms of the GNU General Public License as published by
  12. ;;; the Free Software Foundation; either version 3 of the License, or (at
  13. ;;; your option) any later version.
  14. ;;;
  15. ;;; GNU Guix is distributed in the hope that it will be useful, but
  16. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;;; GNU General Public License for more details.
  19. ;;;
  20. ;;; You should have received a copy of the GNU General Public License
  21. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  22. (define-module (guix import cpan)
  23. #:use-module (ice-9 match)
  24. #:use-module (ice-9 regex)
  25. #:use-module ((ice-9 popen) #:select (open-pipe* close-pipe))
  26. #:use-module ((ice-9 rdelim) #:select (read-line))
  27. #:use-module (srfi srfi-1)
  28. #:use-module (srfi srfi-26)
  29. #:use-module (json)
  30. #:use-module (gcrypt hash)
  31. #:use-module (guix store)
  32. #:use-module (guix utils)
  33. #:use-module (guix base32)
  34. #:use-module (guix ui)
  35. #:use-module ((guix download) #:select (download-to-store url-fetch))
  36. #:use-module ((guix import utils) #:select (factorize-uri))
  37. #:use-module (guix import json)
  38. #:use-module (guix packages)
  39. #:use-module (guix upstream)
  40. #:use-module (guix derivations)
  41. #:export (cpan-dependency?
  42. cpan-dependency-relationship
  43. cpan-dependency-phase
  44. cpan-dependency-module
  45. cpan-dependency-version
  46. cpan-release?
  47. cpan-release-license
  48. cpan-release-author
  49. cpan-release-version
  50. cpan-release-module
  51. cpan-release-distribution
  52. cpan-release-download-url
  53. cpan-release-abstract
  54. cpan-release-home-page
  55. cpan-release-dependencies
  56. json->cpan-release
  57. cpan-fetch
  58. cpan->guix-package
  59. metacpan-url->mirror-url
  60. %cpan-updater
  61. %metacpan-base-url))
  62. ;;; Commentary:
  63. ;;;
  64. ;;; Generate a package declaration template for the latest version of a CPAN
  65. ;;; module, using meta-data from metacpan.org.
  66. ;;;
  67. ;;; Code:
  68. (define %metacpan-base-url
  69. ;; Base URL of the MetaCPAN API.
  70. (make-parameter "https://fastapi.metacpan.org/v1/"))
  71. ;; Dependency of a "release".
  72. (define-json-mapping <cpan-dependency> make-cpan-dependency cpan-dependency?
  73. json->cpan-dependency
  74. (relationship cpan-dependency-relationship "relationship"
  75. string->symbol) ;requires | suggests
  76. (phase cpan-dependency-phase "phase"
  77. string->symbol) ;develop | configure | test | runtime
  78. (module cpan-dependency-module) ;string
  79. (version cpan-dependency-version)) ;string
  80. ;; Release as returned by <https://fastapi.metacpan.org/v1/release/PKG>.
  81. (define-json-mapping <cpan-release> make-cpan-release cpan-release?
  82. json->cpan-release
  83. (license cpan-release-license)
  84. (author cpan-release-author)
  85. (version cpan-release-version "version"
  86. (match-lambda
  87. ((? number? version)
  88. ;; Version is sometimes not quoted in the module json, so
  89. ;; it gets imported into Guile as a number, so convert it
  90. ;; to a string (example: "X11-Protocol-Other").
  91. (number->string version))
  92. ((? string? version)
  93. ;; Sometimes we get a "v" prefix. Strip it.
  94. (if (string-prefix? "v" version)
  95. (string-drop version 1)
  96. version))))
  97. (module cpan-release-module "main_module") ;e.g., "Test::Script"
  98. (distribution cpan-release-distribution) ;e.g., "Test-Script"
  99. (download-url cpan-release-download-url "download_url")
  100. (abstract cpan-release-abstract "abstract")
  101. (home-page cpan-release-home-page "resources"
  102. (match-lambda
  103. (#f #f)
  104. ((? unspecified?) #f)
  105. ((lst ...) (assoc-ref lst "homepage"))))
  106. (dependencies cpan-release-dependencies "dependency"
  107. (lambda (vector)
  108. (map json->cpan-dependency (vector->list vector)))))
  109. (define string->license
  110. (match-lambda
  111. ;; List of valid values from https://metacpan.org/pod/CPAN::Meta::Spec.
  112. ;; Some licenses are excluded based on their absense from (guix licenses).
  113. ("agpl_3" 'agpl3)
  114. ;; apache_1_1
  115. ("apache_2_0" 'asl2.0)
  116. ;; artistic_1
  117. ("artistic_2" 'artistic2.0)
  118. ("bsd" 'bsd-3)
  119. ("freebsd" 'bsd-2)
  120. ;; gfdl_1_2
  121. ("gfdl_1_3" 'fdl1.3+)
  122. ("gpl_1" 'gpl1)
  123. ("gpl_2" 'gpl2)
  124. ("gpl_3" 'gpl3)
  125. ("lgpl_2_1" 'lgpl2.1)
  126. ("lgpl_3_0" 'lgpl3)
  127. ("mit" 'x11)
  128. ;; mozilla_1_0
  129. ("mozilla_1_1" 'mpl1.1)
  130. ("openssl" 'openssl)
  131. ("perl_5" 'perl-license) ;GPL1+ and Artistic 1
  132. ("qpl_1_0" 'qpl)
  133. ;; ssleay
  134. ;; sun
  135. ("zlib" 'zlib)
  136. (#(x) (string->license x))
  137. (#(lst ...) `(list ,@(map string->license lst)))
  138. (_ #f)))
  139. (define (module->name module)
  140. "Transform a 'module' name into a 'release' name"
  141. (regexp-substitute/global #f "::" module 'pre "-" 'post))
  142. (define (module->dist-name module)
  143. "Return the base distribution module for a given module. E.g. the 'ok'
  144. module is distributed with 'Test::Simple', so (module->dist-name \"ok\") would
  145. return \"Test-Simple\""
  146. (assoc-ref (json-fetch (string-append
  147. (%metacpan-base-url) "/module/"
  148. module
  149. "?fields=distribution"))
  150. "distribution"))
  151. (define (package->upstream-name package)
  152. "Return the CPAN name of PACKAGE."
  153. (let* ((properties (package-properties package))
  154. (upstream-name (and=> properties
  155. (cut assoc-ref <> 'upstream-name))))
  156. (or upstream-name
  157. (match (package-source package)
  158. ((? origin? origin)
  159. (match (origin-uri origin)
  160. ((or (? string? url) (url _ ...))
  161. (match (string-match (string-append "([^/]*)-v?[0-9\\.]+") url)
  162. (#f #f)
  163. (m (match:substring m 1))))
  164. (_ #f)))
  165. (_ #f)))))
  166. (define (cpan-fetch name)
  167. "Return a <cpan-release> record for Perl module MODULE,
  168. or #f on failure. MODULE should be the distribution name, such as
  169. \"Test-Script\" for the \"Test::Script\" module."
  170. ;; This API always returns the latest release of the module.
  171. (and=> (json-fetch (string-append (%metacpan-base-url) "/release/"
  172. name))
  173. json->cpan-release))
  174. (define (cpan-home name)
  175. (string-append "https://metacpan.org/release/" name))
  176. (define (metacpan-url->mirror-url url)
  177. "Replace 'https://cpan.metacpan.org' in URL with 'mirror://cpan'."
  178. (regexp-substitute/global #f "http[s]?://cpan.metacpan.org"
  179. url
  180. 'pre "mirror://cpan" 'post))
  181. (define cpan-source-url
  182. (compose metacpan-url->mirror-url cpan-release-download-url))
  183. (define (perl-package)
  184. "Return the 'perl' package. This is a lazy reference so that we don't
  185. depend on (gnu packages perl)."
  186. (module-ref (resolve-interface '(gnu packages perl)) 'perl))
  187. (define %corelist
  188. (delay
  189. (let* ((perl (with-store store
  190. (derivation->output-path
  191. (package-derivation store (perl-package)))))
  192. (core (string-append perl "/bin/corelist")))
  193. (and (access? core X_OK)
  194. core))))
  195. (define core-module?
  196. (let ((rx (make-regexp
  197. (string-append "released with perl v?([0-9\\.]*)"
  198. "(.*and removed from v?([0-9\\.]*))?"))))
  199. (lambda (name)
  200. (define perl-version
  201. (package-version (perl-package)))
  202. (define (version-between? lower version upper)
  203. (and (version>=? version lower)
  204. (or (not upper)
  205. (version>? upper version))))
  206. (and (force %corelist)
  207. (parameterize ((current-error-port (%make-void-port "w")))
  208. (let* ((corelist (open-pipe* OPEN_READ (force %corelist) name)))
  209. (let loop ()
  210. (let ((line (read-line corelist)))
  211. (if (eof-object? line)
  212. (begin (close-pipe corelist) #f)
  213. (or (and=> (regexp-exec rx line)
  214. (lambda (m)
  215. (let ((first (match:substring m 1))
  216. (last (match:substring m 3)))
  217. (version-between?
  218. first perl-version last))))
  219. (loop)))))))))))
  220. (define (cpan-module->sexp release)
  221. "Return the 'package' s-expression for a CPAN module from the release data
  222. in RELEASE, a <cpan-release> record."
  223. (define name
  224. (cpan-release-distribution release))
  225. (define (guix-name name)
  226. (if (string-prefix? "perl-" name)
  227. (string-downcase name)
  228. (string-append "perl-" (string-downcase name))))
  229. (define version (cpan-release-version release))
  230. (define source-url (cpan-source-url release))
  231. (define (convert-inputs phases)
  232. ;; Convert phase dependencies into a list of name/variable pairs.
  233. (match (filter-map (lambda (dependency)
  234. (and (memq (cpan-dependency-phase dependency)
  235. phases)
  236. (cpan-dependency-module dependency)))
  237. (cpan-release-dependencies release))
  238. ((inputs ...)
  239. (sort
  240. (delete-duplicates
  241. ;; Listed dependencies may include core modules. Filter those out.
  242. (filter-map (match-lambda
  243. ("perl" #f) ;implicit dependency
  244. ((? core-module?) #f)
  245. (module
  246. (let ((name (guix-name (module->dist-name module))))
  247. (list name
  248. (list 'unquote (string->symbol name))))))
  249. inputs))
  250. (lambda args
  251. (match args
  252. (((a _ ...) (b _ ...))
  253. (string<? a b))))))))
  254. (define (maybe-inputs guix-name inputs)
  255. (match inputs
  256. (()
  257. '())
  258. ((inputs ...)
  259. (list (list guix-name
  260. (list 'quasiquote inputs))))))
  261. (let ((tarball (with-store store
  262. (download-to-store store source-url))))
  263. `(package
  264. (name ,(guix-name name))
  265. (version ,version)
  266. (source (origin
  267. (method url-fetch)
  268. (uri (string-append ,@(factorize-uri source-url version)))
  269. (sha256
  270. (base32
  271. ,(bytevector->nix-base32-string (file-sha256 tarball))))))
  272. (build-system perl-build-system)
  273. ,@(maybe-inputs 'native-inputs
  274. ;; "runtime" may also be needed here. See
  275. ;; https://metacpan.org/pod/CPAN::Meta::Spec#Phases,
  276. ;; which says they are required during building. We
  277. ;; have not yet had a need for cross-compiled perl
  278. ;; modules, however, so we leave it out.
  279. (convert-inputs '(configure build test)))
  280. ,@(maybe-inputs 'propagated-inputs
  281. (convert-inputs '(runtime)))
  282. (home-page ,(cpan-home name))
  283. (synopsis ,(cpan-release-abstract release))
  284. (description fill-in-yourself!)
  285. (license ,(string->license (cpan-release-license release))))))
  286. (define (cpan->guix-package module-name)
  287. "Fetch the metadata for PACKAGE-NAME from metacpan.org, and return the
  288. `package' s-expression corresponding to that package, or #f on failure."
  289. (let ((release (cpan-fetch (module->name module-name))))
  290. (and=> release cpan-module->sexp)))
  291. (define cpan-package?
  292. (let ((cpan-rx (make-regexp (string-append "("
  293. "mirror://cpan" "|"
  294. "https?://www.cpan.org" "|"
  295. "https?://cpan.metacpan.org"
  296. ")"))))
  297. (url-predicate (cut regexp-exec cpan-rx <>))))
  298. (define (latest-release package)
  299. "Return an <upstream-source> for the latest release of PACKAGE."
  300. (match (cpan-fetch (package->upstream-name package))
  301. (#f #f)
  302. (release
  303. (let ((core-inputs
  304. (match (package-direct-inputs package)
  305. (((_ inputs _ ...) ...)
  306. (filter-map (match-lambda
  307. ((and (? package?)
  308. (? cpan-package?)
  309. (= package->upstream-name
  310. (? core-module? name)))
  311. name)
  312. (else #f))
  313. inputs)))))
  314. ;; Warn about inputs that are part of perl's core
  315. (unless (null? core-inputs)
  316. (for-each (lambda (module)
  317. (warning (G_ "input '~a' of ~a is in Perl core~%")
  318. module (package-name package)))
  319. core-inputs)))
  320. (let ((version (cpan-release-version release))
  321. (url (cpan-source-url release)))
  322. (upstream-source
  323. (package (package-name package))
  324. (version version)
  325. (urls (list url)))))))
  326. (define %cpan-updater
  327. (upstream-updater
  328. (name 'cpan)
  329. (description "Updater for CPAN packages")
  330. (pred cpan-package?)
  331. (latest latest-release)))