cpan.scm 14 KB

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