contentdb.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2021 Maxime Devos <maximedevos@telenet;be>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  6. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; GNU Guix is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;;; GNU General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. (define-module (guix import contentdb)
  19. #:use-module (ice-9 match)
  20. #:use-module (ice-9 receive)
  21. #:use-module (srfi srfi-1)
  22. #:use-module (srfi srfi-2)
  23. #:use-module (srfi srfi-11)
  24. #:use-module (srfi srfi-26)
  25. #:use-module (guix utils)
  26. #:use-module (guix memoization)
  27. #:use-module (guix serialization)
  28. #:use-module (guix import utils)
  29. #:use-module (guix import json)
  30. #:use-module ((gcrypt hash) #:select (open-sha256-port port-sha256))
  31. #:use-module (json)
  32. #:use-module (guix base32)
  33. #:use-module (guix git)
  34. #:use-module (guix store)
  35. #:use-module ((guix licenses) #:prefix license:)
  36. #:export (%contentdb-api
  37. contentdb->guix-package
  38. contentdb-recursive-import))
  39. ;; The ContentDB API is documented at
  40. ;; <https://content.minetest.net>.
  41. (define %contentdb-api
  42. (make-parameter "https://content.minetest.net/api/"))
  43. (define (string-or-false x)
  44. (and (string? x) x))
  45. (define (natural-or-false x)
  46. (and (exact-integer? x) (>= x 0) x))
  47. ;; Descriptions on ContentDB use carriage returns, but Guix doesn't.
  48. (define (delete-cr text)
  49. (string-delete #\cr text))
  50. ;; Minetest package.
  51. ;;
  52. ;; API endpoint: /packages/AUTHOR/NAME/
  53. (define-json-mapping <package> make-package package?
  54. json->package
  55. (author package-author) ; string
  56. (creation-date package-creation-date ; string
  57. "created_at")
  58. (downloads package-downloads) ; integer
  59. (forums package-forums "forums" natural-or-false) ; natural | #f
  60. (issue-tracker package-issue-tracker "issue_tracker") ; string
  61. (license package-license) ; string
  62. (long-description package-long-description "long_description") ; string
  63. (maintainers package-maintainers ; list of strings
  64. "maintainers" vector->list)
  65. (media-license package-media-license "media_license") ; string
  66. (name package-name) ; string
  67. (provides package-provides ; list of strings
  68. "provides" vector->list)
  69. (release package-release) ; integer
  70. (repository package-repository "repo" string-or-false) ; string | #f
  71. (score package-score) ; flonum
  72. (screenshots package-screenshots "screenshots" vector->list) ; list of strings
  73. (short-description package-short-description "short_description") ; string
  74. (state package-state) ; string
  75. (tags package-tags "tags" vector->list) ; list of strings
  76. (thumbnail package-thumbnail) ; string
  77. (title package-title) ; string
  78. (type package-type) ; string
  79. (url package-url) ; string
  80. (website package-website "website" string-or-false)) ; string | #f
  81. (define-json-mapping <release> make-release release?
  82. json->release
  83. (commit release-commit "commit" string-or-false) ; string | #f
  84. (downloads release-downloads) ; integer
  85. (id release-id) ; integer
  86. (max-minetest-version release-max-minetest-version) ; string | #f
  87. (min-minetest-version release-min-minetest-version) ; string | #f
  88. (release-date release-data) ; string
  89. (title release-title) ; string
  90. (url release-url)) ; string
  91. (define-json-mapping <dependency> make-dependency dependency?
  92. json->dependency
  93. (optional? dependency-optional? "is_optional") ; #t | #f
  94. (name dependency-name) ; string
  95. (packages dependency-packages "packages" vector->list)) ; list of strings
  96. (define (contentdb-fetch author name)
  97. "Return a <package> record for package NAME by AUTHOR, or #f on failure."
  98. (and=> (json-fetch
  99. (string-append (%contentdb-api) "packages/" author "/" name "/"))
  100. json->package))
  101. (define (contentdb-fetch-releases author name)
  102. "Return a list of <release> records for package NAME by AUTHOR, or #f
  103. on failure."
  104. (and=> (json-fetch (string-append (%contentdb-api) "packages/" author "/" name
  105. "/releases/"))
  106. (lambda (json)
  107. (map json->release (vector->list json)))))
  108. (define (latest-release author name)
  109. "Return the latest source release for package NAME by AUTHOR,
  110. or #f if this package does not exist."
  111. (and=> (contentdb-fetch-releases author name)
  112. car))
  113. (define (contentdb-fetch-dependencies author name)
  114. "Return an alist of lists of <dependency> records for package NAME by AUTHOR
  115. and possibly some other packages as well, or #f on failure."
  116. (define url (string-append (%contentdb-api) "packages/" author "/" name
  117. "/dependencies/"))
  118. (and=> (json-fetch url)
  119. (lambda (json)
  120. (map (match-lambda
  121. ((key . value)
  122. (cons key (map json->dependency (vector->list value)))))
  123. json))))
  124. (define (contentdb->package-name name)
  125. "Given the NAME of a package on ContentDB, return a Guix-compliant name for the
  126. package."
  127. ;; The author is not included, as the names of popular mods
  128. ;; tend to be unique.
  129. (string-append "minetest-" (snake-case name)))
  130. ;; XXX copied from (guix import elpa)
  131. (define* (download-git-repository url ref)
  132. "Fetch the given REF from the Git repository at URL."
  133. (with-store store
  134. (latest-repository-commit store url #:ref ref)))
  135. ;; XXX adapted from (guix scripts hash)
  136. (define (file-hash file select? recursive?)
  137. ;; Compute the hash of FILE.
  138. (if recursive?
  139. (let-values (((port get-hash) (open-sha256-port)))
  140. (write-file file port #:select? select?)
  141. (force-output port)
  142. (get-hash))
  143. (call-with-input-file file port-sha256)))
  144. ;; XXX likewise.
  145. (define (vcs-file? file stat)
  146. (case (stat:type stat)
  147. ((directory)
  148. (member (basename file) '(".bzr" ".git" ".hg" ".svn" "CVS")))
  149. ((regular)
  150. ;; Git sub-modules have a '.git' file that is a regular text file.
  151. (string=? (basename file) ".git"))
  152. (else
  153. #f)))
  154. (define (make-minetest-sexp name version repository commit
  155. inputs home-page synopsis
  156. description media-license license)
  157. "Return a S-expression for the minetest package with the given NAME,
  158. VERSION, REPOSITORY, COMMIT, INPUTS, HOME-PAGE, SYNOPSIS, DESCRIPTION,
  159. MEDIA-LICENSE and LICENSE."
  160. `(package
  161. (name ,(contentdb->package-name name))
  162. (version ,version)
  163. (source
  164. (origin
  165. (method git-fetch)
  166. (uri (git-reference
  167. (url ,repository)
  168. (commit ,commit)))
  169. (sha256
  170. (base32
  171. ;; The commit id is not always available.
  172. ,(and commit
  173. (bytevector->nix-base32-string
  174. (file-hash
  175. (download-git-repository repository `(commit . ,commit))
  176. (negate vcs-file?) #t)))))
  177. (file-name (git-file-name name version))))
  178. (build-system minetest-mod-build-system)
  179. ,@(maybe-propagated-inputs
  180. (map (compose contentdb->package-name cdr) inputs))
  181. (home-page ,home-page)
  182. (synopsis ,(delete-cr synopsis))
  183. (description ,(delete-cr description))
  184. (license ,(if (eq? media-license license)
  185. (license->symbol license)
  186. `(list ,(license->symbol media-license)
  187. ,(license->symbol license))))))
  188. (define (package-home-page package)
  189. "Guess the home page of the ContentDB package PACKAGE.
  190. In order of preference, try the 'website', the forum topic on the
  191. official Minetest forum and the Git repository (if any)."
  192. (define (topic->url-sexp topic)
  193. ;; 'minetest-topic' is a procedure defined in (gnu packages minetest)
  194. `(minetest-topic ,topic))
  195. (or (package-website package)
  196. (and=> (package-forums package) topic->url-sexp)
  197. (package-repository package)))
  198. (define (important-dependencies dependencies author name)
  199. (define dependency-list
  200. (assoc-ref dependencies (string-append author "/" name)))
  201. (filter-map
  202. (lambda (dependency)
  203. (and (not (dependency-optional? dependency))
  204. ;; "default" must be provided by the 'subgame' in use
  205. ;; and does not refer to a specific minetest mod.
  206. ;; "doors", "bucket" ... are provided by the default minetest
  207. ;; subgame.
  208. (not (member (dependency-name dependency)
  209. '("default" "doors" "beds" "bucket" "doors" "farming"
  210. "flowers" "stairs" "xpanes")))
  211. ;; Dependencies often have only one implementation.
  212. (let* ((/name (string-append "/" (dependency-name dependency)))
  213. (likewise-named-implementations
  214. (filter (cut string-suffix? /name <>)
  215. (dependency-packages dependency)))
  216. (implementation
  217. (and (not (null? likewise-named-implementations))
  218. (first likewise-named-implementations))))
  219. (and implementation
  220. (apply cons (string-split implementation #\/))))))
  221. dependency-list))
  222. (define* (%contentdb->guix-package author name)
  223. "Fetch the metadata for NAME by AUTHOR from https://content.minetest.net, and
  224. return the 'package' S-expression corresponding to that package, or #f on failure.
  225. On success, also return the upstream dependencies as a list of
  226. (AUTHOR . NAME) pairs."
  227. (and-let* ((package (contentdb-fetch author name))
  228. (dependencies (contentdb-fetch-dependencies author name))
  229. (release (latest-release author name)))
  230. (let ((important-upstream-dependencies
  231. (important-dependencies dependencies author name)))
  232. (values (make-minetest-sexp name
  233. (release-title release) ; version
  234. (package-repository package)
  235. (release-commit release)
  236. important-upstream-dependencies
  237. (package-home-page package)
  238. (package-short-description package)
  239. (package-long-description package)
  240. (string->license
  241. (package-media-license package))
  242. (string->license
  243. (package-license package)))
  244. important-upstream-dependencies))))
  245. (define contentdb->guix-package
  246. (memoize %contentdb->guix-package))
  247. (define (contentdb-recursive-import author name)
  248. ;; recursive-import expects upstream package names to be strings,
  249. ;; so do some conversions.
  250. (define (split-author/name author/name)
  251. (string-split author/name #\/))
  252. (define (author+name->author/name author+name)
  253. (string-append (car author+name) "/" (cdr author+name)))
  254. (define* (contentdb->guix-package* author/name #:key repo version)
  255. (receive (package . maybe-dependencies)
  256. (apply contentdb->guix-package (split-author/name author/name))
  257. (and package
  258. (receive (dependencies)
  259. (apply values maybe-dependencies)
  260. (values package
  261. (map author+name->author/name dependencies))))))
  262. (recursive-import (author+name->author/name (cons author name))
  263. #:repo->guix-package contentdb->guix-package*
  264. #:guix-name
  265. (lambda (author/name)
  266. (contentdb->package-name
  267. (second (split-author/name author/name))))))
  268. ;; A list of license names is available at
  269. ;; <https://content.minetest.net/api/licenses/>.
  270. (define (string->license str)
  271. "Convert the string STR into a license object."
  272. (match str
  273. ("GPLv3" license:gpl3)
  274. ("GPLv2" license:gpl2)
  275. ("ISC" license:isc)
  276. ;; "MIT" means the Expat license on ContentDB,
  277. ;; see <https://github.com/minetest/contentdb/issues/326#issuecomment-890143784>.
  278. ("MIT" license:expat)
  279. ("CC BY-SA 3.0" license:cc-by-sa3.0)
  280. ("CC BY-SA 4.0" license:cc-by-sa4.0)
  281. ("LGPLv2.1" license:lgpl2.1)
  282. ("LGPLv3" license:lgpl3)
  283. ("MPL 2.0" license:mpl2.0)
  284. ("ZLib" license:zlib)
  285. ("Unlicense" license:unlicense)
  286. (_ #f)))