minetest.scm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 minetest)
  19. #:use-module (ice-9 match)
  20. #:use-module (ice-9 receive)
  21. #:use-module (ice-9 threads)
  22. #:use-module (ice-9 hash-table)
  23. #:use-module (srfi srfi-1)
  24. #:use-module (srfi srfi-2)
  25. #:use-module (srfi srfi-11)
  26. #:use-module (srfi srfi-26)
  27. #:use-module (guix utils)
  28. #:use-module (guix ui)
  29. #:use-module (guix i18n)
  30. #:use-module (guix memoization)
  31. #:use-module (guix serialization)
  32. #:use-module (guix import utils)
  33. #:use-module (guix import json)
  34. #:use-module ((gcrypt hash) #:select (open-sha256-port port-sha256))
  35. #:use-module (json)
  36. #:use-module (guix base32)
  37. #:use-module (guix git)
  38. #:use-module (guix store)
  39. #:export (%default-sort-key
  40. %contentdb-api
  41. json->package
  42. contentdb-fetch
  43. elaborate-contentdb-name
  44. minetest->guix-package
  45. minetest-recursive-import
  46. sort-packages))
  47. ;; The ContentDB API is documented at
  48. ;; <https://content.minetest.net>.
  49. (define %contentdb-api
  50. (make-parameter "https://content.minetest.net/api/"))
  51. (define (string-or-false x)
  52. (and (string? x) x))
  53. (define (natural-or-false x)
  54. (and (exact-integer? x) (>= x 0) x))
  55. ;; Descriptions on ContentDB use carriage returns, but Guix doesn't.
  56. (define (delete-cr text)
  57. (string-delete #\cr text))
  58. ;;;
  59. ;;; JSON mappings
  60. ;;;
  61. ;; Minetest package.
  62. ;;
  63. ;; API endpoint: /packages/AUTHOR/NAME/
  64. (define-json-mapping <package> make-package package?
  65. json->package
  66. (author package-author) ; string
  67. (creation-date package-creation-date ; string
  68. "created_at")
  69. (downloads package-downloads) ; integer
  70. (forums package-forums "forums" natural-or-false)
  71. (issue-tracker package-issue-tracker "issue_tracker") ; string
  72. (license package-license) ; string
  73. (long-description package-long-description "long_description") ; string
  74. (maintainers package-maintainers ; list of strings
  75. "maintainers" vector->list)
  76. (media-license package-media-license "media_license") ; string
  77. (name package-name) ; string
  78. (provides package-provides ; list of strings
  79. "provides" vector->list)
  80. (release package-release) ; integer
  81. (repository package-repository "repo" string-or-false)
  82. (score package-score) ; flonum
  83. (screenshots package-screenshots "screenshots" vector->list) ; list of strings
  84. (short-description package-short-description "short_description") ; string
  85. (state package-state) ; string
  86. (tags package-tags "tags" vector->list) ; list of strings
  87. (thumbnail package-thumbnail) ; string
  88. (title package-title) ; string
  89. (type package-type) ; string
  90. (url package-url) ; string
  91. (website package-website "website" string-or-false))
  92. (define-json-mapping <release> make-release release?
  93. json->release
  94. ;; If present, a git commit identified by its hash
  95. (commit release-commit "commit" string-or-false)
  96. (downloads release-downloads) ; integer
  97. (id release-id) ; integer
  98. (max-minetest-version release-max-minetest-version string-or-false)
  99. (min-minetest-version release-min-minetest-version string-or-false)
  100. (release-date release-data) ; string
  101. (title release-title) ; string
  102. (url release-url)) ; string
  103. (define-json-mapping <dependency> make-dependency dependency?
  104. json->dependency
  105. (optional? dependency-optional? "is_optional") ; bool
  106. (name dependency-name) ; string
  107. (packages dependency-packages "packages" vector->list)) ; list of strings
  108. ;; A structure returned by the /api/packages/?fmt=keys endpoint
  109. (define-json-mapping <package-keys> make-package-keys package-keys?
  110. json->package-keys
  111. (author package-keys-author) ; string
  112. (name package-keys-name) ; string
  113. (type package-keys-type)) ; string
  114. (define (package-mod? package)
  115. "Is the ContentDB package PACKAGE a mod?"
  116. ;; ContentDB also has ‘games’ and ‘texture packs’.
  117. (string=? (package-type package) "mod"))
  118. ;;;
  119. ;;; Manipulating names of packages
  120. ;;;
  121. ;;; There are three kind of names:
  122. ;;;
  123. ;;; * names of guix packages, e.g. minetest-basic-materials.
  124. ;;; * names of mods on ContentDB, e.g. basic_materials
  125. ;;; * a combination of author and mod name on ContentDB, e.g. VanessaE/basic_materials
  126. ;;;
  127. (define (%construct-full-name author name)
  128. (string-append author "/" name))
  129. (define (package-full-name package)
  130. "Given a <package> object, return the corresponding AUTHOR/NAME string."
  131. (%construct-full-name (package-author package) (package-name package)))
  132. (define (package-keys-full-name package)
  133. "Given a <package-keys> object, return the corresponding AUTHOR/NAME string."
  134. (%construct-full-name (package-keys-author package)
  135. (package-keys-name package)))
  136. (define (contentdb->package-name author/name)
  137. "Given the AUTHOR/NAME of a package on ContentDB, return a Guix-compliant
  138. name for the package."
  139. ;; The author is not included, as the names of popular mods
  140. ;; tend to be unique.
  141. (string-append "minetest-" (snake-case (author/name->name author/name))))
  142. (define (author/name->name author/name)
  143. "Extract NAME from the AUTHOR/NAME string, or raise an error if AUTHOR/NAME
  144. is ill-formatted."
  145. (match (string-split author/name #\/)
  146. ((author name)
  147. (when (string-null? author)
  148. (leave
  149. (G_ "In ~a: author names must consist of at least a single character.~%")
  150. author/name))
  151. (when (string-null? name)
  152. (leave
  153. (G_ "In ~a: mod names must consist of at least a single character.~%")
  154. author/name))
  155. name)
  156. ((too many . components)
  157. (leave
  158. (G_ "In ~a: author names and mod names may not contain forward slashes.~%")
  159. author/name))
  160. ((name)
  161. (if (string-null? name)
  162. (leave (G_ "mod names may not be empty.~%"))
  163. (leave (G_ "The name of the author is missing in ~a.~%")
  164. author/name)))))
  165. (define* (elaborate-contentdb-name name #:key (sort %default-sort-key))
  166. "If NAME is an AUTHOR/NAME string, return it. Otherwise, try to determine
  167. the author and return an appropriate AUTHOR/NAME string. If that fails,
  168. raise an exception."
  169. (if (or (string-contains name "/") (string-null? name))
  170. ;; Call 'author/name->name' to verify that NAME seems reasonable
  171. ;; and raise an appropriate exception if it isn't.
  172. (begin
  173. (author/name->name name)
  174. name)
  175. (let* ((package-keys (contentdb-query-packages name #:sort sort))
  176. (correctly-named
  177. (filter (lambda (package-key)
  178. (string=? name (package-keys-name package-key)))
  179. package-keys)))
  180. (match correctly-named
  181. ((one) (package-keys-full-name one))
  182. ((too . many)
  183. (warning (G_ "~a is ambigious, presuming ~a (other options include: ~a)~%")
  184. name (package-keys-full-name too)
  185. (map package-keys-full-name many))
  186. (package-keys-full-name too))
  187. (()
  188. (leave (G_ "No mods with name ~a were found.~%") name))))))
  189. ;;;
  190. ;;; API endpoints
  191. ;;;
  192. (define contentdb-fetch
  193. (mlambda (author/name)
  194. "Return a <package> record for package AUTHOR/NAME, or #f on failure."
  195. (and=> (json-fetch
  196. (string-append (%contentdb-api) "packages/" author/name "/"))
  197. json->package)))
  198. (define (contentdb-fetch-releases author/name)
  199. "Return a list of <release> records for package NAME by AUTHOR, or #f
  200. on failure."
  201. (and=> (json-fetch (string-append (%contentdb-api) "packages/" author/name
  202. "/releases/"))
  203. (lambda (json)
  204. (map json->release (vector->list json)))))
  205. (define (latest-release author/name)
  206. "Return the latest source release for package NAME by AUTHOR,
  207. or #f if this package does not exist."
  208. (and=> (contentdb-fetch-releases author/name)
  209. car))
  210. (define (contentdb-fetch-dependencies author/name)
  211. "Return an alist of lists of <dependency> records for package NAME by AUTHOR
  212. and possibly some other packages as well, or #f on failure."
  213. (define url (string-append (%contentdb-api) "packages/" author/name
  214. "/dependencies/"))
  215. (and=> (json-fetch url)
  216. (lambda (json)
  217. (map (match-lambda
  218. ((key . value)
  219. (cons key (map json->dependency (vector->list value)))))
  220. json))))
  221. (define* (contentdb-query-packages q #:key
  222. (type "mod")
  223. (limit 50)
  224. (sort %default-sort-key)
  225. (order "desc"))
  226. "Search ContentDB for Q (a string). Sort by SORT, in ascending order
  227. if ORDER is \"asc\" or descending order if ORDER is \"desc\". TYPE must
  228. be \"mod\", \"game\" or \"txp\", restricting thes search results to
  229. respectively mods, games and texture packs. Limit to at most LIMIT
  230. results. The return value is a list of <package-keys> records."
  231. ;; XXX does Guile have something for constructing (and, when necessary,
  232. ;; escaping) query strings?
  233. (define url (string-append (%contentdb-api) "packages/?type=" type
  234. "&q=" q "&fmt=keys"
  235. "&limit=" (number->string limit)
  236. "&order=" order
  237. "&sort=" sort))
  238. (let ((json (json-fetch url)))
  239. (if json
  240. (map json->package-keys (vector->list json))
  241. (leave
  242. (G_ "The package search API doesn't exist anymore.~%")))))
  243. ;; XXX copied from (guix import elpa)
  244. (define* (download-git-repository url ref)
  245. "Fetch the given REF from the Git repository at URL."
  246. (with-store store
  247. (latest-repository-commit store url #:ref ref)))
  248. ;; XXX adapted from (guix scripts hash)
  249. (define (file-hash file)
  250. "Compute the hash of FILE."
  251. (let-values (((port get-hash) (open-sha256-port)))
  252. (write-file file port)
  253. (force-output port)
  254. (get-hash)))
  255. (define (make-minetest-sexp author/name version repository commit
  256. inputs home-page synopsis
  257. description media-license license)
  258. "Return a S-expression for the minetest package with the given author/NAME,
  259. VERSION, REPOSITORY, COMMIT, INPUTS, HOME-PAGE, SYNOPSIS, DESCRIPTION,
  260. MEDIA-LICENSE and LICENSE."
  261. `(package
  262. (name ,(contentdb->package-name author/name))
  263. (version ,version)
  264. (source
  265. (origin
  266. (method git-fetch)
  267. (uri (git-reference
  268. (url ,repository)
  269. (commit ,commit)))
  270. (sha256
  271. (base32
  272. ;; The git commit is not always available.
  273. ,(and commit
  274. (bytevector->nix-base32-string
  275. (file-hash
  276. (download-git-repository repository
  277. `(commit . ,commit)))))))
  278. (file-name (git-file-name name version))))
  279. (build-system minetest-mod-build-system)
  280. ,@(maybe-propagated-inputs (map contentdb->package-name inputs))
  281. (home-page ,home-page)
  282. (synopsis ,(delete-cr synopsis))
  283. (description ,(delete-cr description))
  284. (license ,(if (eq? media-license license)
  285. license
  286. `(list ,media-license ,license)))
  287. ;; The Minetest updater (not yet in Guix; it requires not-yet-submitted
  288. ;; patches to (guix upstream) that require some work) needs to know both
  289. ;; the author name and mod name for efficiency.
  290. (properties ,(list 'quasiquote `((upstream-name . ,author/name))))))
  291. (define (package-home-page package)
  292. "Guess the home page of the ContentDB package PACKAGE.
  293. In order of preference, try the 'website', the forum topic on the
  294. official Minetest forum and the Git repository (if any)."
  295. (define (topic->url-sexp topic)
  296. ;; 'minetest-topic' is a procedure defined in (gnu packages minetest)
  297. `(minetest-topic ,topic))
  298. (or (package-website package)
  299. (and=> (package-forums package) topic->url-sexp)
  300. (package-repository package)))
  301. ;; If the default sort key is changed, make sure to modify 'show-help'
  302. ;; in (guix scripts import minetest) appropriately as well.
  303. (define %default-sort-key "score")
  304. (define* (sort-packages packages #:key (sort %default-sort-key))
  305. "Sort PACKAGES by SORT, in descending order."
  306. (define package->key
  307. (match sort
  308. ("score" package-score)
  309. ("downloads" package-downloads)))
  310. (define (greater x y)
  311. (> (package->key x) (package->key y)))
  312. (sort-list packages greater))
  313. (define builtin-mod?
  314. (let ((%builtin-mods
  315. (alist->hash-table
  316. (map (lambda (x) (cons x #t))
  317. '("beds" "binoculars" "boats" "bones" "bucket" "butterflies"
  318. "carts" "creative" "default" "doors" "dungeon_loot" "dye"
  319. "env_sounds" "farming" "fire" "fireflies" "flowers"
  320. "game_commands" "give_initial_stuff" "map" "mtg_craftguide"
  321. "player_api" "screwdriver" "sethome" "sfinv" "spawn" "stairs"
  322. "tnt" "vessels" "walls" "weather" "wool" "xpanes")))))
  323. (lambda (mod)
  324. "Is MOD provided by the default minetest subgame?"
  325. (hash-ref %builtin-mods mod))))
  326. (define* (important-dependencies dependencies author/name
  327. #:key (sort %default-sort-key))
  328. "Return the hard dependencies of AUTHOR/NAME in the association list
  329. DEPENDENCIES as a list of AUTHOR/NAME strings."
  330. (define dependency-list
  331. (assoc-ref dependencies author/name))
  332. (filter-map
  333. (lambda (dependency)
  334. (and (not (dependency-optional? dependency))
  335. (not (builtin-mod? (dependency-name dependency)))
  336. ;; The dependency information contains symbolic names
  337. ;; that can be ‘provided’ by multiple mods, so we need to choose one
  338. ;; of the implementations.
  339. (let* ((implementations
  340. (par-map contentdb-fetch (dependency-packages dependency)))
  341. ;; Fetching package information about the packages is racy:
  342. ;; some packages might be removed from ContentDB between the
  343. ;; construction of DEPENDENCIES and the call to
  344. ;; 'contentdb-fetch'. So filter out #f.
  345. ;;
  346. ;; Filter out ‘games’ that include the requested mod -- it's
  347. ;; the mod itself we want.
  348. (mods (filter (lambda (p) (and=> p package-mod?))
  349. implementations))
  350. (sorted-mods (sort-packages mods #:sort sort)))
  351. (match sorted-mods
  352. ((package) (package-full-name package))
  353. ((too . many)
  354. (warning
  355. (G_ "The dependency ~a of ~a has multiple different implementations ~a.~%")
  356. (dependency-name dependency)
  357. author/name
  358. (map package-full-name sorted-mods))
  359. (match sort
  360. ("score"
  361. (warning
  362. (G_ "The implementation with the highest score will be choosen!~%")))
  363. ("downloads"
  364. (warning
  365. (G_ "The implementation that has been downloaded the most will be choosen!~%"))))
  366. (package-full-name too))
  367. (()
  368. (warning
  369. (G_ "The dependency ~a of ~a does not have any implementation. It will be ignored!~%")
  370. (dependency-name dependency) author/name)
  371. #f)))))
  372. dependency-list))
  373. (define* (%minetest->guix-package author/name #:key (sort %default-sort-key))
  374. "Fetch the metadata for AUTHOR/NAME from https://content.minetest.net, and
  375. return the 'package' S-expression corresponding to that package, or raise an
  376. exception on failure. On success, also return the upstream dependencies as a
  377. list of AUTHOR/NAME strings."
  378. ;; Call 'author/name->name' to verify that AUTHOR/NAME seems reasonable.
  379. (author/name->name author/name)
  380. (define package (contentdb-fetch author/name))
  381. (unless package
  382. (leave (G_ "no package metadata for ~a on ContentDB~%") author/name))
  383. (define dependencies (contentdb-fetch-dependencies author/name))
  384. (unless dependencies
  385. (leave (G_ "no dependency information for ~a on ContentDB~%") author/name))
  386. (define release (latest-release author/name))
  387. (unless release
  388. (leave (G_ "no release of ~a on ContentDB~%") author/name))
  389. (define important-upstream-dependencies
  390. (important-dependencies dependencies author/name #:sort sort))
  391. (values (make-minetest-sexp author/name
  392. (release-title release) ; version
  393. (package-repository package)
  394. (release-commit release)
  395. important-upstream-dependencies
  396. (package-home-page package)
  397. (package-short-description package)
  398. (package-long-description package)
  399. (spdx-string->license
  400. (package-media-license package))
  401. (spdx-string->license
  402. (package-license package)))
  403. important-upstream-dependencies))
  404. (define minetest->guix-package
  405. (memoize %minetest->guix-package))
  406. (define* (minetest-recursive-import author/name #:key (sort %default-sort-key))
  407. (define* (minetest->guix-package* author/name #:key repo version)
  408. (minetest->guix-package author/name #:sort sort))
  409. (recursive-import author/name
  410. #:repo->guix-package minetest->guix-package*
  411. #:guix-name contentdb->package-name))