minetest.scm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2021, 2022 Maxime Devos <maximedevos@telenet.be>
  3. ;;; Copyright © 2022 Hartmut Goebel <h.goebel@crazy-compilers.com>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix import minetest)
  20. #:use-module (ice-9 match)
  21. #:use-module (ice-9 threads)
  22. #:use-module (ice-9 hash-table)
  23. #:use-module (srfi srfi-1)
  24. #:use-module (srfi srfi-34)
  25. #:use-module (guix diagnostics)
  26. #:use-module ((guix packages) #:prefix package:)
  27. #:use-module (guix upstream)
  28. #:use-module (guix i18n)
  29. #:use-module (guix memoization)
  30. #:use-module (guix serialization)
  31. #:use-module (guix import utils)
  32. #:use-module (guix import json)
  33. #:use-module (json)
  34. #:use-module (guix base32)
  35. #:use-module (guix git)
  36. #:use-module ((guix git-download) #:prefix download:)
  37. #:use-module (guix hash)
  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-package?
  45. latest-minetest-release
  46. minetest->guix-package
  47. minetest-recursive-import
  48. sort-packages
  49. %minetest-updater))
  50. ;; The ContentDB API is documented at
  51. ;; <https://content.minetest.net>.
  52. (define %contentdb-api
  53. (make-parameter "https://content.minetest.net/api/"))
  54. (define (string-or-false x)
  55. (and (string? x) x))
  56. (define (natural-or-false x)
  57. (and (exact-integer? x) (>= x 0) x))
  58. ;; Descriptions on ContentDB use carriage returns, but Guix doesn't.
  59. (define (delete-cr text)
  60. (string-delete #\cr text))
  61. ;;;
  62. ;;; JSON mappings
  63. ;;;
  64. ;; Minetest package.
  65. ;;
  66. ;; API endpoint: /packages/AUTHOR/NAME/
  67. (define-json-mapping <package> make-package package?
  68. json->package
  69. (author package-author) ; string
  70. (creation-date package-creation-date ; string
  71. "created_at")
  72. (downloads package-downloads) ; integer
  73. (forums package-forums "forums" natural-or-false)
  74. (issue-tracker package-issue-tracker "issue_tracker") ; string
  75. (license package-license) ; string
  76. (long-description package-long-description "long_description") ; string
  77. (maintainers package-maintainers ; list of strings
  78. "maintainers" vector->list)
  79. (media-license package-media-license "media_license") ; string
  80. (name package-name) ; string
  81. (provides package-provides ; list of strings
  82. "provides" vector->list)
  83. (release package-release) ; integer
  84. (repository package-repository "repo" string-or-false)
  85. (score package-score) ; flonum
  86. (screenshots package-screenshots "screenshots" vector->list) ; list of strings
  87. (short-description package-short-description "short_description") ; string
  88. (state package-state) ; string
  89. (tags package-tags "tags" vector->list) ; list of strings
  90. (thumbnail package-thumbnail) ; string
  91. (title package-title) ; string
  92. (type package-type) ; string
  93. (url package-url) ; string
  94. (website package-website "website" string-or-false))
  95. (define-json-mapping <release> make-release release?
  96. json->release
  97. ;; If present, a git commit identified by its hash
  98. (commit release-commit "commit" string-or-false)
  99. (downloads release-downloads) ; integer
  100. (id release-id) ; integer
  101. (max-minetest-version release-max-minetest-version string-or-false)
  102. (min-minetest-version release-min-minetest-version string-or-false)
  103. (release-date release-data) ; string
  104. (title release-title) ; string
  105. (url release-url)) ; string
  106. (define-json-mapping <dependency> make-dependency dependency?
  107. json->dependency
  108. (optional? dependency-optional? "is_optional") ; bool
  109. (name dependency-name) ; string
  110. (packages dependency-packages "packages" vector->list)) ; list of strings
  111. ;; A structure returned by the /api/packages/?fmt=keys endpoint
  112. (define-json-mapping <package-keys> make-package-keys package-keys?
  113. json->package-keys
  114. (author package-keys-author) ; string
  115. (name package-keys-name) ; string
  116. (type package-keys-type)) ; string
  117. (define (package-mod? package)
  118. "Is the ContentDB package PACKAGE a mod?"
  119. ;; ContentDB also has ‘games’ and ‘texture packs’.
  120. (string=? (package-type package) "mod"))
  121. ;;;
  122. ;;; Manipulating names of packages
  123. ;;;
  124. ;;; There are three kind of names:
  125. ;;;
  126. ;;; * names of guix packages, e.g. minetest-basic-materials.
  127. ;;; * names of mods on ContentDB, e.g. basic_materials
  128. ;;; * a combination of author and mod name on ContentDB, e.g. VanessaE/basic_materials
  129. ;;;
  130. (define (%construct-full-name author name)
  131. (string-append author "/" name))
  132. (define (package-full-name package)
  133. "Given a <package> object, return the corresponding AUTHOR/NAME string."
  134. (%construct-full-name (package-author package) (package-name package)))
  135. (define (package-keys-full-name package)
  136. "Given a <package-keys> object, return the corresponding AUTHOR/NAME string."
  137. (%construct-full-name (package-keys-author package)
  138. (package-keys-name package)))
  139. (define (contentdb->package-name author/name)
  140. "Given the AUTHOR/NAME of a package on ContentDB, return a Guix-compliant
  141. name for the package."
  142. ;; The author is not included, as the names of popular mods
  143. ;; tend to be unique.
  144. (string-append "minetest-" (snake-case (author/name->name author/name))))
  145. (define (author/name->name author/name)
  146. "Extract NAME from the AUTHOR/NAME string, or raise an error if AUTHOR/NAME
  147. is ill-formatted."
  148. (match (string-split author/name #\/)
  149. ((author name)
  150. (when (string-null? author)
  151. (leave
  152. (G_ "In ~a: author names must consist of at least a single character.~%")
  153. author/name))
  154. (when (string-null? name)
  155. (leave
  156. (G_ "In ~a: mod names must consist of at least a single character.~%")
  157. author/name))
  158. name)
  159. ((too many . components)
  160. (leave
  161. (G_ "In ~a: author names and mod names may not contain forward slashes.~%")
  162. author/name))
  163. ((name)
  164. (if (string-null? name)
  165. (leave (G_ "mod names may not be empty.~%"))
  166. (leave (G_ "The name of the author is missing in ~a.~%")
  167. author/name)))))
  168. (define* (elaborate-contentdb-name name #:key (sort %default-sort-key))
  169. "If NAME is an AUTHOR/NAME string, return it. Otherwise, try to determine
  170. the author and return an appropriate AUTHOR/NAME string. If that fails,
  171. raise an exception."
  172. (if (or (string-contains name "/") (string-null? name))
  173. ;; Call 'author/name->name' to verify that NAME seems reasonable
  174. ;; and raise an appropriate exception if it isn't.
  175. (begin
  176. (author/name->name name)
  177. name)
  178. (let* ((package-keys (contentdb-query-packages name #:sort sort))
  179. (correctly-named
  180. (filter (lambda (package-key)
  181. (string=? name (package-keys-name package-key)))
  182. package-keys)))
  183. (match correctly-named
  184. ((one) (package-keys-full-name one))
  185. ((too . many)
  186. (warning (G_ "~a is ambiguous, presuming ~a (other options include: ~a)~%")
  187. name (package-keys-full-name too)
  188. (map package-keys-full-name many))
  189. (package-keys-full-name too))
  190. (()
  191. (leave (G_ "No mods with name ~a were found.~%") name))))))
  192. ;;;
  193. ;;; API endpoints
  194. ;;;
  195. (define contentdb-fetch
  196. (mlambda (author/name)
  197. "Return a <package> record for package AUTHOR/NAME, or #f on failure."
  198. (and=> (json-fetch
  199. (string-append (%contentdb-api) "packages/" author/name "/"))
  200. json->package)))
  201. (define (contentdb-fetch-releases author/name)
  202. "Return a list of <release> records for package NAME by AUTHOR, or #f
  203. on failure."
  204. (and=> (json-fetch (string-append (%contentdb-api) "packages/" author/name
  205. "/releases/"))
  206. (lambda (json)
  207. (map json->release (vector->list json)))))
  208. (define (latest-release author/name)
  209. "Return the latest source release for package NAME by AUTHOR,
  210. or #f if this package does not exist."
  211. (and=> (contentdb-fetch-releases author/name)
  212. car))
  213. (define (contentdb-fetch-dependencies author/name)
  214. "Return an alist of lists of <dependency> records for package NAME by AUTHOR
  215. and possibly some other packages as well, or #f on failure."
  216. (define url (string-append (%contentdb-api) "packages/" author/name
  217. "/dependencies/"))
  218. (and=> (json-fetch url)
  219. (lambda (json)
  220. (map (match-lambda
  221. ((key . value)
  222. (cons key (map json->dependency (vector->list value)))))
  223. json))))
  224. (define* (contentdb-query-packages q #:key
  225. (type "mod")
  226. (limit 50)
  227. (sort %default-sort-key)
  228. (order "desc"))
  229. "Search ContentDB for Q (a string). Sort by SORT, in ascending order
  230. if ORDER is \"asc\" or descending order if ORDER is \"desc\". TYPE must
  231. be \"mod\", \"game\" or \"txp\", restricting the search results to
  232. respectively mods, games and texture packs. Limit to at most LIMIT
  233. results. The return value is a list of <package-keys> records."
  234. ;; XXX does Guile have something for constructing (and, when necessary,
  235. ;; escaping) query strings?
  236. (define url (string-append (%contentdb-api) "packages/?type=" type
  237. "&q=" q "&fmt=keys"
  238. "&limit=" (number->string limit)
  239. "&order=" order
  240. "&sort=" sort))
  241. (let ((json (json-fetch url)))
  242. (if json
  243. (map json->package-keys (vector->list json))
  244. (leave
  245. (G_ "The package search API doesn't exist anymore.~%")))))
  246. ;; XXX copied from (guix import elpa)
  247. (define* (download-git-repository url ref)
  248. "Fetch the given REF from the Git repository at URL."
  249. (with-store store
  250. (latest-repository-commit store url #:ref ref)))
  251. (define (make-minetest-sexp author/name version repository commit
  252. inputs home-page synopsis
  253. description media-license license)
  254. "Return a S-expression for the minetest package with the given author/NAME,
  255. VERSION, REPOSITORY, COMMIT, INPUTS, HOME-PAGE, SYNOPSIS, DESCRIPTION,
  256. MEDIA-LICENSE and LICENSE."
  257. `(package
  258. (name ,(contentdb->package-name author/name))
  259. (version ,version)
  260. (source
  261. (origin
  262. (method git-fetch)
  263. (uri (git-reference
  264. (url ,repository)
  265. (commit ,commit)))
  266. (sha256
  267. (base32
  268. ;; The git commit is not always available.
  269. ,(and commit
  270. (bytevector->nix-base32-string
  271. (file-hash*
  272. (download-git-repository repository
  273. `(commit . ,commit))
  274. ;; 'download-git-repository' already filtered out the '.git'
  275. ;; directory.
  276. #:select? (const #true)
  277. #:recursive? #true)))))
  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 ,(beautify-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. (define (release-version release)
  302. "Guess the version of RELEASE from the release title."
  303. (define title (release-title release))
  304. (if (string-prefix? "v" title)
  305. ;; Remove "v" prefix from release titles like ‘v1.0.1’.
  306. (substring title 1)
  307. title))
  308. (define (version-style version)
  309. "Determine the kind of version number VERSION is -- a date, or a conventional
  310. conventional version number."
  311. (define dots? (->bool (string-index version #\.)))
  312. (define hyphens? (->bool (string-index version #\-)))
  313. (match (cons dots? hyphens?)
  314. ((#true . #false) 'regular) ; something like "0.1"
  315. ((#false . #false) 'regular) ; single component version number
  316. ((#true . #true) 'regular) ; result of 'git-version'
  317. ((#false . #true) 'date))) ; something like "2021-01-25"
  318. ;; If the default sort key is changed, make sure to modify 'show-help'
  319. ;; in (guix scripts import minetest) appropriately as well.
  320. (define %default-sort-key "score")
  321. (define* (sort-packages packages #:key (sort %default-sort-key))
  322. "Sort PACKAGES by SORT, in descending order."
  323. (define package->key
  324. (match sort
  325. ("score" package-score)
  326. ("downloads" package-downloads)))
  327. (define (greater x y)
  328. (> (package->key x) (package->key y)))
  329. (sort-list packages greater))
  330. (define builtin-mod?
  331. (let ((%builtin-mods
  332. (alist->hash-table
  333. (map (lambda (x) (cons x #t))
  334. '("beds" "binoculars" "boats" "bones" "bucket" "butterflies"
  335. "carts" "creative" "default" "doors" "dungeon_loot" "dye"
  336. "env_sounds" "farming" "fire" "fireflies" "flowers"
  337. "game_commands" "give_initial_stuff" "map" "mtg_craftguide"
  338. "player_api" "screwdriver" "sethome" "sfinv" "spawn" "stairs"
  339. "tnt" "vessels" "walls" "weather" "wool" "xpanes")))))
  340. (lambda (mod)
  341. "Is MOD provided by the default minetest subgame?"
  342. (hash-ref %builtin-mods mod))))
  343. (define* (important-dependencies dependencies author/name
  344. #:key (sort %default-sort-key))
  345. "Return the hard dependencies of AUTHOR/NAME in the association list
  346. DEPENDENCIES as a list of AUTHOR/NAME strings."
  347. (define dependency-list
  348. (assoc-ref dependencies author/name))
  349. ;; A mod can have multiple dependencies implemented by the same mod,
  350. ;; so remove duplicate mod names.
  351. (define (filter-deduplicate-map f list)
  352. (delete-duplicates (filter-map f list)))
  353. (filter-deduplicate-map
  354. (lambda (dependency)
  355. (and (not (dependency-optional? dependency))
  356. (not (builtin-mod? (dependency-name dependency)))
  357. ;; The dependency information contains symbolic names
  358. ;; that can be ‘provided’ by multiple mods, so we need to choose one
  359. ;; of the implementations.
  360. (let* ((implementations
  361. (par-map contentdb-fetch (dependency-packages dependency)))
  362. ;; Fetching package information about the packages is racy:
  363. ;; some packages might be removed from ContentDB between the
  364. ;; construction of DEPENDENCIES and the call to
  365. ;; 'contentdb-fetch'. So filter out #f.
  366. ;;
  367. ;; Filter out ‘games’ that include the requested mod -- it's
  368. ;; the mod itself we want.
  369. (mods (filter (lambda (p) (and=> p package-mod?))
  370. implementations))
  371. (sorted-mods (sort-packages mods #:sort sort)))
  372. (match sorted-mods
  373. ((package) (package-full-name package))
  374. ((too . many)
  375. (warning
  376. (G_ "The dependency ~a of ~a has multiple different implementations ~a.~%")
  377. (dependency-name dependency)
  378. author/name
  379. (map package-full-name sorted-mods))
  380. (match sort
  381. ("score"
  382. (warning
  383. (G_ "The implementation with the highest score will be chosen!~%")))
  384. ("downloads"
  385. (warning
  386. (G_ "The implementation that has been downloaded the most will be chosen!~%"))))
  387. (package-full-name too))
  388. (()
  389. (warning
  390. (G_ "The dependency ~a of ~a does not have any implementation. It will be ignored!~%")
  391. (dependency-name dependency) author/name)
  392. #f)))))
  393. dependency-list))
  394. (define* (%minetest->guix-package author/name #:key (sort %default-sort-key)
  395. #:allow-other-keys)
  396. "Fetch the metadata for AUTHOR/NAME from https://content.minetest.net, and
  397. return the 'package' S-expression corresponding to that package, or raise an
  398. exception on failure. On success, also return the upstream dependencies as a
  399. list of AUTHOR/NAME strings."
  400. ;; Call 'author/name->name' to verify that AUTHOR/NAME seems reasonable.
  401. (author/name->name author/name)
  402. (define package (contentdb-fetch author/name))
  403. (unless package
  404. (leave (G_ "no package metadata for ~a on ContentDB~%") author/name))
  405. (define dependencies (contentdb-fetch-dependencies author/name))
  406. (unless dependencies
  407. (leave (G_ "no dependency information for ~a on ContentDB~%") author/name))
  408. (define release (latest-release author/name))
  409. (unless release
  410. (leave (G_ "no release of ~a on ContentDB~%") author/name))
  411. (define important-upstream-dependencies
  412. (important-dependencies dependencies author/name #:sort sort))
  413. (values (make-minetest-sexp author/name
  414. (release-version release)
  415. (package-repository package)
  416. (release-commit release)
  417. important-upstream-dependencies
  418. (package-home-page package)
  419. (package-short-description package)
  420. (package-long-description package)
  421. (spdx-string->license
  422. (package-media-license package))
  423. (spdx-string->license
  424. (package-license package)))
  425. important-upstream-dependencies))
  426. (define minetest->guix-package
  427. (memoize %minetest->guix-package))
  428. (define* (minetest-recursive-import author/name #:key (sort %default-sort-key))
  429. (define* (minetest->guix-package* author/name #:key version #:allow-other-keys)
  430. (minetest->guix-package author/name #:sort sort))
  431. (recursive-import author/name
  432. #:repo->guix-package minetest->guix-package*
  433. #:guix-name contentdb->package-name))
  434. (define (minetest-package? pkg)
  435. "Is PKG a Minetest mod on ContentDB?"
  436. (and (string-prefix? "minetest-" (package:package-name pkg))
  437. (assq-ref (package:package-properties pkg) 'upstream-name)))
  438. (define* (latest-minetest-release pkg #:key (version #f))
  439. "Return an <upstream-source> for the latest release of the package PKG,
  440. or #false if the latest release couldn't be determined."
  441. (define author/name
  442. (assq-ref (package:package-properties pkg) 'upstream-name))
  443. (define contentdb-package (contentdb-fetch author/name)) ; TODO warn if #f?
  444. (define release (latest-release author/name))
  445. (define source (package:package-source pkg))
  446. (when version
  447. (raise
  448. (formatted-message
  449. (G_ "~a updater doesn't support updating to a specific version, sorry.")
  450. "minetest")))
  451. (and contentdb-package release
  452. (release-commit release) ; not always set
  453. ;; Only continue if both the old and new version number are both
  454. ;; dates or regular version numbers, as two different styles confuses
  455. ;; the logic for determining which version is newer.
  456. (eq? (version-style (release-version release))
  457. (version-style (package:package-version pkg)))
  458. (upstream-source
  459. (package (package:package-name pkg))
  460. (version (release-version release))
  461. (urls (download:git-reference
  462. (url (package-repository contentdb-package))
  463. (commit (release-commit release)))))))
  464. (define %minetest-updater
  465. (upstream-updater
  466. (name 'minetest)
  467. (description "Updater for Minetest packages on ContentDB")
  468. (pred minetest-package?)
  469. (import latest-minetest-release)))