1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 inherit (lib)
9 mkIf
10 mkEnableOption
11 mkPackageOption
12 mkOption
13 literalExpression
14 hasAttr
15 toList
16 length
17 head
18 tail
19 concatStringsSep
20 optionalString
21 optionalAttrs
22 isDerivation
23 recursiveUpdate
24 getExe
25 types
26 maintainers
27 ;
28 cfg = config.services.wivrn;
29 configFormat = pkgs.formats.json { };
30
31 # For the application option to work with systemd PATH, we find the store binary path of
32 # the package, concat all of the following strings, and then update the application attribute.
33
34 # Since the json config attribute type "configFormat.type" doesn't allow specifying types for
35 # individual attributes, we have to type check manually.
36
37 # The application option should be a list with package as the first element, though a single package is also valid.
38 # Note that this module depends on the package containing the meta.mainProgram attribute.
39
40 # Check if an application is provided
41 applicationAttrExists = hasAttr "application" cfg.config.json;
42 applicationList = toList cfg.config.json.application;
43 applicationListNotEmpty = length applicationList != 0;
44 applicationCheck = applicationAttrExists && applicationListNotEmpty;
45
46 # Manage packages and their exe paths
47 applicationAttr = head applicationList;
48 applicationPackage = mkIf applicationCheck applicationAttr;
49 applicationPackageExe = getExe applicationAttr;
50 serverPackageExe = (
51 if cfg.highPriority then "${config.security.wrapperDir}/wivrn-server" else getExe cfg.package
52 );
53
54 # Manage strings
55 applicationStrings = tail applicationList;
56 applicationConcat = concatStringsSep " " ([ applicationPackageExe ] ++ applicationStrings);
57
58 # Manage config file
59 applicationUpdate = recursiveUpdate cfg.config.json (
60 optionalAttrs applicationCheck { application = applicationConcat; }
61 );
62 configFile = configFormat.generate "config.json" applicationUpdate;
63 enabledConfig = optionalString cfg.config.enable "-f ${configFile}";
64
65 # Manage server executables and flags
66 serverExec = concatStringsSep " " (
67 [
68 serverPackageExe
69 "--systemd"
70 enabledConfig
71 ]
72 ++ cfg.extraServerFlags
73 );
74in
75{
76 options = {
77 services.wivrn = {
78 enable = mkEnableOption "WiVRn, an OpenXR streaming application";
79
80 package = mkPackageOption pkgs "wivrn" { };
81
82 openFirewall = mkEnableOption "the default ports in the firewall for the WiVRn server";
83
84 defaultRuntime = mkEnableOption ''
85 WiVRn as the default OpenXR runtime on the system.
86 The config can be found at `/etc/xdg/openxr/1/active_runtime.json`.
87
88 Note that applications can bypass this option by setting an active
89 runtime in a writable XDG_CONFIG_DIRS location like `~/.config`
90 '';
91
92 autoStart = mkEnableOption "starting the service by default";
93
94 highPriority = mkEnableOption "high priority capability for asynchronous reprojection";
95
96 monadoEnvironment = mkOption {
97 type = types.attrs;
98 description = "Environment variables to be passed to the Monado environment.";
99 default = { };
100 };
101
102 extraServerFlags = mkOption {
103 type = types.listOf types.str;
104 description = "Flags to add to the wivrn service.";
105 default = [ ];
106 example = literalExpression ''[ "--no-publish-service" ]'';
107 };
108
109 steam = {
110 importOXRRuntimes = mkEnableOption ''
111 Sets `PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES` system-wide to allow Steam to automatically discover the WiVRn server.
112
113 Note that you may have to logout for this variable to be visible
114 '';
115
116 package = mkPackageOption pkgs "steam" { };
117 };
118
119 config = {
120 enable = mkEnableOption "configuration for WiVRn";
121 json = mkOption {
122 type = configFormat.type;
123 description = ''
124 Configuration for WiVRn. The attributes are serialized to JSON in config.json. The server will fallback to default values for any missing attributes.
125
126 Like upstream, the application option is a list including the application and it's flags. In the case of the NixOS module however, the first element of the list must be a package. The module will assert otherwise.
127 The application can be set to a single package because it gets passed to lib.toList, though this will not allow for flags to be passed.
128
129 See <https://github.com/WiVRn/WiVRn/blob/master/docs/configuration.md>
130 '';
131 default = { };
132 example = literalExpression ''
133 {
134 scale = 0.5;
135 bitrate = 100000000;
136 encoders = [
137 {
138 encoder = "nvenc";
139 codec = "h264";
140 width = 1.0;
141 height = 1.0;
142 offset_x = 0.0;
143 offset_y = 0.0;
144 }
145 ];
146 application = [ pkgs.wlx-overlay-s ];
147 }
148 '';
149 };
150 };
151 };
152 };
153
154 config = mkIf cfg.enable {
155 assertions = [
156 {
157 assertion = !applicationCheck || isDerivation applicationAttr;
158 message = "The application in WiVRn configuration is not a package. Please ensure that the application is a package or that a package is the first element in the list.";
159 }
160 ];
161
162 security.wrappers."wivrn-server" = mkIf cfg.highPriority {
163 setuid = false;
164 owner = "root";
165 group = "root";
166 capabilities = "cap_sys_nice+eip";
167 source = getExe cfg.package;
168 };
169
170 systemd.user = {
171 services = {
172 wivrn = {
173 description = "WiVRn XR runtime service";
174 environment = recursiveUpdate {
175 # Default options
176 # https://gitlab.freedesktop.org/monado/monado/-/blob/598080453545c6bf313829e5780ffb7dde9b79dc/src/xrt/targets/service/monado.in.service#L12
177 XRT_COMPOSITOR_LOG = "debug";
178 XRT_PRINT_OPTIONS = "on";
179 IPC_EXIT_ON_DISCONNECT = "off";
180 PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES = mkIf cfg.steam.importOXRRuntimes "1";
181 } cfg.monadoEnvironment;
182 serviceConfig = (
183 if cfg.highPriority then
184 {
185 ExecStart = serverExec;
186 }
187 # Hardening options break high-priority
188 else
189 {
190 ExecStart = serverExec;
191 # Hardening options
192 CapabilityBoundingSet = [ "CAP_SYS_NICE" ];
193 AmbientCapabilities = [ "CAP_SYS_NICE" ];
194 LockPersonality = true;
195 NoNewPrivileges = true;
196 PrivateTmp = true;
197 ProtectClock = true;
198 ProtectControlGroups = true;
199 ProtectKernelLogs = true;
200 ProtectKernelModules = true;
201 ProtectKernelTunables = true;
202 ProtectProc = "invisible";
203 ProtectSystem = "strict";
204 RemoveIPC = true;
205 RestrictNamespaces = true;
206 RestrictSUIDSGID = true;
207 }
208 );
209 path = [ cfg.steam.package ];
210 wantedBy = mkIf cfg.autoStart [ "default.target" ];
211 restartTriggers = [
212 cfg.package
213 cfg.steam.package
214 ];
215 };
216 };
217 };
218
219 services = {
220 udev.packages = with pkgs; [ android-udev-rules ];
221 avahi = {
222 enable = true;
223 publish = {
224 enable = true;
225 userServices = true;
226 };
227 };
228 };
229
230 networking.firewall = mkIf cfg.openFirewall {
231 allowedTCPPorts = [ 9757 ];
232 allowedUDPPorts = [ 9757 ];
233 };
234
235 environment = {
236 systemPackages = [
237 cfg.package
238 applicationPackage
239 ];
240 sessionVariables = mkIf cfg.steam.importOXRRuntimes {
241 PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES = "1";
242 };
243 pathsToLink = [ "/share/openxr" ];
244 etc."xdg/openxr/1/active_runtime.json" = mkIf cfg.defaultRuntime {
245 source = "${cfg.package}/share/openxr/1/openxr_wivrn.json";
246 };
247 };
248 };
249 meta.maintainers = with maintainers; [ passivelemon ];
250}