guix_mirror_bot pushed a commit to branch master
in repository guix.

commit 47af617b5cc1318d6d2422eb2ef962d417a3625c
Author: Maxim Cournoyer <[email protected]>
AuthorDate: Mon Dec 29 11:27:06 2025 +0900

    services: Add luanti-service-type.
    
    * gnu/services/games.scm (luanti-configuration): New variable.
    (%luanti-account): Likewise.
    (luanti-activation): New procedure.
    (luanti-shepherd-service): Likewise.
    (luanti-service-type): New variable.
    * gnu/tests/games.scm: New file.
    
    Change-Id: I65a1dcf832fa8add9c9d278d82bab91ca3eef086
    Reviewed-by: Liliana Marie Prikler <[email protected]>
---
 CODEOWNERS             |   2 +
 doc/guix.texi          |  85 +++++++++++++++++++
 etc/teams.scm          |   2 +
 gnu/services/games.scm | 215 ++++++++++++++++++++++++++++++++++++++++++++++++-
 gnu/tests/games.scm    | 108 +++++++++++++++++++++++++
 5 files changed, 411 insertions(+), 1 deletion(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index da3e644b5a..35b29b2607 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -170,6 +170,8 @@ gnu/packages/game-development\.scm                 
@guix/games
 gnu/packages/luanti\.scm                           @guix/games
 gnu/packages/esolangs\.scm                         @guix/games
 gnu/packages/motti\.scm                            @guix/games
+gnu/services/games\.scm                            @guix/games
+gnu/tests/games\.scm                               @guix/games
 guix/build/luanti-build-system\.scm                @guix/games
 
 etc/teams/gnome                                    @guix/gnome
diff --git a/doc/guix.texi b/doc/guix.texi
index c504ec06cd..bd3c73824d 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -43209,6 +43209,91 @@ the @code{joycond-configuration} configuration), so 
that joycond
 controllers can be detected and used by an unprivileged user.
 @end defvar
 
+@subsubheading Luanti service
+@cindex luanti
+@cindex voxel-based games
+@uref{https://www.luanti.org/en/, Luanti} is a voxel game engine that
+powers many games.  This service is for hosting a Luanti server.  The
+various options can be configured via the @code{luanti-configuration}
+record, documented below:
+
+@c %start of fragment
+
+@deftp {Data Type} luanti-configuration
+Available @code{luanti-configuration} fields are:
+
+@table @asis
+@item @code{luanti} (default: @code{luanti-server}) (type: file-like)
+The Luanti package to use.
+
+@item @code{game} (default: @code{luanti-mineclonia}) (type: file-like)
+The Luanti game package to serve.
+
+@item @code{game-configuration} (type: maybe-file-like)
+A configuration file to use for the selected Luanti game, which
+corresponds to the @file{minetest.conf} file.
+
+@item @code{mods} (type: maybe-list-of-file-likes)
+A list of Luanti mod packages to use.  Note that using mods is
+complicated by the requirements of Luanti to 1) manually enable the mod
+and any of its dependent mods in the @file{world.rt} file of the world
+used and 2) to register the mod names and those of its dependents via a
+@samp{secure.trusted_mods} @code{game-configuration} directive.  Consult
+the example below for more precise directions.
+
+@item @code{log-file} (default: @code{"/var/log/luanti.log"}) (type: 
maybe-string)
+The log file to log to.  To disable logging, set this to
+@code{%unset-value}.
+
+@item @code{verbose?} (default: @code{#f}) (type: boolean)
+Print more detailed information.
+
+@item @code{port} (default: @code{30000}) (type: port)
+The UDP port the server should listen to.
+
+@item @code{world} (type: maybe-string)
+An existing Luanti world directory to serve.  If omitted, a new world is
+created under the @file{/var/lib/luanti/.minetest/worlds/world}
+directory.  If an absolute file name is provided, it is used directly.
+Otherwise, it is expected to be a directory under
+@file{/var/lib/luanti/.minetest/worlds/}.
+
+@end table
+
+@end deftp
+
+
+@c %end of fragment
+
+Here's the simplest example of a Luanti server, which in its default
+configuration serves the @code{luanti-mineclonia} game.
+
+@lisp
+(service luanti-service-type)
+@end lisp
+
+Here's a slightly more elaborate one, which adds the
+@code{luanti-whitelist} mod.  Embedded are comments explaining extra
+needed steps when using mods.  Failing to do these steps will cause the
+service to fail to start.
+
+@lisp
+(service luanti-service-type
+         (luanti-configuration
+          (game luanti-mineclonia)
+          (game-configuration
+           (plain-file
+            "minetest.conf"
+            ;; lib_chatcmdbuilder is a dependency of the whitelist mod
+            "secure.trusted_mods = whitelist,lib_chatcmdbuilder\n"))
+          ;; The
+          ;; '/var/lib/luanti/.minetest/worlds/world/world.mt'
+          ;; file needs to be hand-edited to add:
+          ;; load_mod_whitelist = true
+          ;; load_mod_lib_chatcmdbuilder = true
+          (mods (list luanti-whitelist))))
+@end lisp
+
 @subsubheading The Battle for Wesnoth Service
 @cindex wesnothd
 @uref{https://wesnoth.org, The Battle for Wesnoth} is a fantasy, turn
diff --git a/etc/teams.scm b/etc/teams.scm
index a5cdbf3e29..30739f3466 100755
--- a/etc/teams.scm
+++ b/etc/teams.scm
@@ -666,6 +666,8 @@ ecosystem."
                       "gnu/packages/luanti.scm"
                       "gnu/packages/esolangs.scm" ; granted, rather niche
                       "gnu/packages/motti.scm"
+                      "gnu/services/games.scm"
+                      "gnu/tests/games.scm"
                       "guix/build/luanti-build-system.scm")))
 
 (define-team gnome
diff --git a/gnu/services/games.scm b/gnu/services/games.scm
index cea442fd3a..9f78a3bc9b 100644
--- a/gnu/services/games.scm
+++ b/gnu/services/games.scm
@@ -1,6 +1,7 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2018 Arun Isaac <[email protected]>
 ;;; Copyright © 2022 Ludovic Courtès <[email protected]>
+;;; Copyright © 2025 Maxim Cournoyer <[email protected]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -23,20 +24,36 @@
   #:use-module (gnu services shepherd)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages games)
+  #:use-module (gnu packages luanti)
   #:use-module ((gnu services base) #:select (udev-service-type))
   #:use-module (gnu system shadow)
   #:use-module ((gnu system file-systems) #:select (file-system-mapping))
   #:use-module (gnu build linux-container)
-  #:autoload   (guix least-authority) (least-authority-wrapper)
+  #:use-module (guix build utils)
+  #:autoload   (guix least-authority) (%default-preserved-environment-variables
+                                       least-authority-wrapper)
   #:use-module (guix gexp)
   #:use-module (guix modules)
   #:use-module (guix packages)
   #:use-module (guix records)
   #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
   #:export (joycond-configuration
             joycond-configuration?
             joycond-service-type
 
+            luanti-configuration
+            luanti-configuration?
+            luanti-configuration-game-configuration
+            luanti-configuration-game
+            luanti-configuration-mods
+            luanti-configuration-log-file
+            luanti-configuration-luanti
+            luanti-configuration-port
+            luanti-configuration-verbose?
+            luanti-configuration-world
+            luanti-service-type
+
             wesnothd-configuration
             wesnothd-configuration?
             wesnothd-service-type))
@@ -72,6 +89,202 @@ install udev rules required to use the controller as an 
unprivileged user.")
    (default-value (joycond-configuration))))
 
 
+;;;
+;;; Luanti.
+;;;
+
+(define list-of-file-likes?
+  (list-of file-like?))
+
+(define-maybe/no-serialization list-of-file-likes)
+
+(define-maybe/no-serialization file-like)
+
+(define-maybe/no-serialization string)
+
+(define (port? x)
+  (and (number? x)
+       (and (>= 0) (<= x 65535))))
+
+(define-configuration/no-serialization luanti-configuration
+  (luanti
+   (file-like luanti-server)
+   "The Luanti package to use.")
+  (game
+   (file-like luanti-mineclonia)
+   "The Luanti game package to serve.")
+  (game-configuration
+   maybe-file-like
+   "A configuration file to use for the selected Luanti game, which
+corresponds to the @file{minetest.conf} file.")
+  (mods
+   maybe-list-of-file-likes
+   "A list of Luanti mod packages to use.  Note that using mods is complicated
+by the requirements of Luanti to 1) manually enable the mod and any of its
+dependent mods in the @file{world.rt} file of the world used and 2) to
+register the mod names and those of its dependents via a
+@samp{secure.trusted_mods} @code{game-configuration} directive.  Consult the
+example below for more precise directions.")
+  (log-file
+   (maybe-string "/var/log/luanti.log")
+   "The log file to log to.  To disable logging, set this to
+@code{%unset-value}.")
+  (verbose?
+   (boolean #f)
+   "Print more detailed information.")
+  (port
+   (port 30000)
+   "The UDP port the server should listen to.")
+  (world
+   maybe-string
+   "An existing Luanti world directory to serve.  If omitted, a new world is
+created under the @file{/var/lib/luanti/.minetest/worlds/world} directory.  If
+an absolute file name is provided, it is used directly.  Otherwise, it is
+expected to be a directory under @file{/var/lib/luanti/.minetest/worlds/}."))
+
+(define %luanti-account
+  (list (user-group
+          (name "luanti")
+          (system? #t))
+        (user-account
+          (name "luanti")
+          (group "luanti")
+          (system? #t)
+          (comment "Luanti server user")
+          (home-directory "/var/lib/luanti"))))
+
+(define (luanti-activation config)
+  "Activation script for the Luanti server."
+  (match-record config <luanti-configuration> (world)
+    #~(begin
+        (use-modules (guix build utils)
+                     (srfi srfi-34))
+
+        (define user (getpwnam "luanti"))
+        (define* (sanitize-permissions file #:optional (mode #o400))
+          (guard (c (#t #t))
+            (chown file (passwd:uid user) (passwd:gid user))
+            (chmod file mode)))
+
+        (mkdir-p/perms "/var/lib/luanti" (getpwnam "luanti") #o755)
+
+        ;; Sanitize the permissions of a provided pre-populated world
+        ;; directory.
+        (when #$(and (maybe-value-set? world)
+                     (absolute-file-name? world))
+          (for-each sanitize-permissions
+                    (find-files #$world #:directories? #t))))))
+
+(define (transitive-mods mods)
+  "Return the transitive list of mods in MODS, these included."
+  (append-map (lambda (m)
+                (if (package? m)
+                    (cons m (map second ;drop label
+                                 (package-transitive-propagated-inputs m)))
+                    (list m)))
+              (if (maybe-value-set? mods)
+                  mods
+                  '())))
+
+(define (luanti-wrapper config)
+  "Return a least-authority wrapper for 'luantiserver', based on CONFIG, a
+<luanti-configuration> object."
+  (match-record config <luanti-configuration>
+                (luanti game game-configuration log-file mods world)
+    (let ((mods (transitive-mods mods)))
+      (least-authority-wrapper
+       (file-append luanti "/bin/luantiserver")
+       #:name "luantiserver-pola-wrapper"
+       #:mappings
+       (let ((readable (filter maybe-value-set?
+                               (append (list luanti game game-configuration)
+                                       mods)))
+             (writable (filter maybe-value-set?
+                               (append (list "/var/lib/luanti" log-file)
+                                       (if (and (maybe-value-set? world)
+                                                (absolute-file-name? world))
+                                           (list world)
+                                           '())))))
+         (append (map (lambda (r)
+                        (file-system-mapping
+                          (source r)
+                          (target source)))
+                      readable)
+                 (map (lambda (w)
+                        (file-system-mapping
+                          (source w)
+                          (target source)
+                          (writable? #t)))
+                      writable)))
+       #:user "luanti"
+       #:group "luanti"
+       ;; XXX: The user namespace must be shared otherwise the UID is different
+       ;; in the container and Luanti fails to create its data directory.
+       #:namespaces (fold delq %namespaces '(user net))
+       #:preserved-environment-variables
+       (cons* "LUANTI_GAME_PATH" "LUANTI_MOD_PATH"
+              %default-preserved-environment-variables)))))
+
+(define (luanti-shepherd-service config)
+  "Return the <shepherd-service> object of Luanti."
+  (match-record config <luanti-configuration>
+                ( luanti game game-configuration log-file mods verbose?
+                  port world)
+    ;; Some mods have dependencies on other mods; we need to ensure these gets
+    ;; added to the LUANTI_MOD_PATH as well.
+    (let ((mods (transitive-mods mods)))
+      (list (shepherd-service
+              (provision '(luanti))
+              (requirement '(user-processes))
+              (start #~(make-forkexec-constructor
+                        (append (list #$(luanti-wrapper config)
+                                      "--port" (number->string #$port))
+                                (if #$(maybe-value-set? game-configuration)
+                                    '("--config" #$game-configuration)
+                                    '())
+                                (if #$verbose?
+                                    '("--verbose")
+                                    '())
+                                (if #$(maybe-value-set? world)
+                                    (if (absolute-file-name? #$world)
+                                        '("--world" #$world)
+                                        '("--worldname" #$world))
+                                    '()))
+                        #:environment-variables
+                        (append
+                         (list "HOME=/var/lib/luanti"
+                               (string-append "LUANTI_GAME_PATH="
+                                              #$game "/share/luanti/games")
+                               (string-append
+                                "LUANTI_MOD_PATH="
+                                (list->search-path-as-string
+                                 (search-path-as-list '("share/luanti/mods")
+                                                      '#$mods)
+                                 ":"))))
+                        #:log-file #$(and (maybe-value-set? log-file)
+                                          log-file)))
+              (stop  #~(make-kill-destructor)))))))
+
+(define luanti-service-type
+  (service-type
+    (name 'luanti)
+    (extensions
+     (list (service-extension shepherd-root-service-type
+                              luanti-shepherd-service)
+           (service-extension profile-service-type
+                              (match-record-lambda <luanti-configuration>
+                                  (luanti game)
+                                (list luanti game)))
+           (service-extension account-service-type
+                              (const %luanti-account))
+           (service-extension activation-service-type
+                              luanti-activation)))
+    (default-value (luanti-configuration))
+    (description
+     "Run @url{https://www.luanti.org/en/, Luanti}, the voxel game engine, as a
+server.")))
+
+
 ;;;
 ;;; The Battle for Wesnoth server
 ;;;
diff --git a/gnu/tests/games.scm b/gnu/tests/games.scm
new file mode 100644
index 0000000000..52391f0caa
--- /dev/null
+++ b/gnu/tests/games.scm
@@ -0,0 +1,108 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Maxim Cournoyer <[email protected]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu tests games)
+  #:use-module (gnu packages luanti)
+  #:use-module (gnu tests)
+  #:use-module (gnu services)
+  #:use-module (gnu services games)
+  #:use-module (gnu system)
+  #:use-module (gnu system vm)
+  #:use-module (guix gexp)
+  #:use-module (guix modules)
+  #:export (%test-luanti))
+
+(define (run-luanti-test name config)
+  "Run a test of an OS running LUANTI-SERVICE."
+  (define os
+    (marionette-operating-system
+     (simple-operating-system
+      (service luanti-service-type config))
+     #:imported-modules '((gnu build dbus-service)
+                          (gnu services herd))))
+
+  (define vm (virtual-machine
+               (operating-system os)
+               (memory-size 1024)))
+
+  (define test
+    (with-imported-modules (source-module-closure
+                            '((gnu build marionette)))
+      #~(begin
+          (use-modules (gnu build marionette)
+                       (srfi srfi-64))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          (test-runner-current (system-test-runner #$output))
+          (test-begin "luanti")
+
+          (test-assert "luanti service can be stopped"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (stop-service 'luanti))
+             marionette))
+
+          (test-assert "luanti service can be started"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (start-service 'luanti))
+             marionette))
+
+          (test-assert "luanti server is responding on configured port"
+            ;; This is based on the Python script example in doc/protocol.txt.
+            (marionette-eval
+             `(begin
+                (use-modules ((gnu build dbus-service) #:select (with-retries))
+                             (gnu services herd)
+                             (ice-9 match)
+                             (rnrs bytevectors)
+                             (rnrs bytevectors gnu))
+
+                (define sock (socket PF_INET SOCK_DGRAM 0))
+                (define addr (make-socket-address AF_INET INADDR_LOOPBACK
+                                                  ,#$(luanti-configuration-port
+                                                      config)))
+                (define probe #vu8(#x4f #x45 #x74 #x03 #x00 #x00 #x00 #x01))
+                (define buf (make-bytevector 1000))
+
+                (with-retries 25 1
+                  (sendto sock probe addr)
+                  (match (select (list sock) '() '() 2) ;limit time to block
+                    (((sock) _ _)
+                     (match (recvfrom! sock buf)
+                       ((byte-count . _)
+                        (and (>= (pk 'byte-count byte-count) 14)
+                             (pk 'peer-id (bytevector-slice buf 12 2)))))))))
+             marionette))
+
+          (test-end))))
+
+  (gexp->derivation name test))
+
+(define %test-luanti
+  (system-test
+   (name "luanti")
+   (description "Connect to a running Luanti server.")
+   (value (run-luanti-test name (luanti-configuration
+                                 (game luanti-mineclonia)
+                                 ;; To test some extra code paths.
+                                 (mods (list luanti-whitelist)))))))

Reply via email to