Cleanup: Remove unused variable
[blender.git] / tests / utils / blender_headless.py
blobc0a2f672cb4fbb04ac6229ef7cec86bc22d25163
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2023 Blender Authors
4 # SPDX-License-Identifier: GPL-2.0-or-later
6 """
7 Wrapper for Blender that launches a graphical instances of Blender
8 in its own display-server.
10 This can be useful when a graphical context is required (when ``--background`` can't be used)
11 and it's preferable not to have windows opening on the user's system.
13 The main use case for this is tests that run simulated events, see: ``bl_run_operators_event_simulate.py``.
15 - All arguments are forwarded to Blender.
16 - Headless operation checks for environment variables.
17 - Blender's exit code is used on exit.
19 Environment Variables:
21 - ``BLENDER_BIN``: the Blender binary to run.
22 (defaults to ``blender`` which must be in the ``PATH``).
23 - ``USE_WINDOW``: When nonzero:
24 Show the window (not actually headless).
25 Useful for troubleshooting so it's possible to see the contents of the window.
26 Note that using a window causes WAYLAND to define a "seat",
27 where the headless session doesn't define a seat.
28 - ``USE_DEBUG``: When nonzero:
29 Run Blender in a debugger.
31 WAYLAND Environment Variables:
33 - ``WESTON_BIN``: The weston binary to run,
34 (defaults to ``weston`` which must be in the ``PATH``).
35 - ``WAYLAND_ROOT_DIR``: The base directory (prefix) of a portable WAYLAND installation,
36 (may be left unset, in that case the system's installed WAYLAND is used).
37 - ``WESTON_ROOT_DIR``: The base directory (prefix) of a portable WESTON installation,
38 (may be left unset, in that case the system's installed WESTON is used).
40 Currently only WAYLAND is supported, other systems could be added.
41 """
42 __all__ = (
43 "main",
46 import subprocess
47 import sys
48 import signal
49 import os
50 import tempfile
52 from typing import (
53 Any,
55 from collections.abc import (
56 Iterator,
57 Sequence,
61 # -----------------------------------------------------------------------------
62 # Constants
64 def environ_nonzero(var: str) -> bool:
65 return os.environ.get(var, "").lstrip("0") != ""
68 BLENDER_BIN = os.environ.get("BLENDER_BIN", "blender")
70 # For debugging, print out all information.
71 VERBOSE = environ_nonzero("VERBOSE")
73 # Show the window in the foreground.
74 USE_WINDOW = environ_nonzero("USE_WINDOW")
75 USE_DEBUG = environ_nonzero("USE_DEBUG")
78 # -----------------------------------------------------------------------------
79 # Generic Utilities
82 def scantree(path: str) -> Iterator[os.DirEntry[str]]:
83 """Recursively yield DirEntry objects for given directory."""
84 for entry in os.scandir(path):
85 if entry.is_dir(follow_symlinks=False):
86 yield from scantree(entry.path)
87 else:
88 yield entry
91 # -----------------------------------------------------------------------------
92 # Implementation Back-Ends
94 class backend_base:
95 @staticmethod
96 def run(args: Sequence[str]) -> int:
97 sys.stderr.write("No headless back-ends for {!r} with args {!r}\n".format(sys.platform, args))
98 return 1
101 class backend_wayland(backend_base):
102 @staticmethod
103 def _wait_for_wayland_server(*, socket: str, timeout: float) -> bool:
105 Uses the expected socket file in `XDG_RUNTIME_DIR` to detect when the WAYLAND server starts.
107 import time
108 time_idle = min(timeout / 100.0, 0.05)
110 xdg_runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "")
111 if not xdg_runtime_dir:
112 xdg_runtime_dir = "/var/run/user/{:d}".format(os.getuid())
114 filepath = os.path.join(xdg_runtime_dir, socket)
116 t_beg = time.time()
117 t_end = t_beg + timeout
118 while True:
119 if os.path.exists(filepath):
120 return True
121 if time.time() >= t_end:
122 break
123 time.sleep(time_idle)
124 return False
126 @staticmethod
127 def _weston_env_and_ini_from_portable(
129 wayland_root_dir: str | None,
130 weston_root_dir: str | None,
131 ) -> tuple[dict[str, str] | None, str]:
133 Construct a portable environment to run WESTON in.
135 # NOTE(@ideasman42): WESTON does not make it convenient to run a portable instance,
136 # a reasonable amount of logic here is simply to get WESTON running with references to portable paths.
137 # Once packages are available on the Linux distribution used for the CI-environment,
138 # we can consider removing this entire function.
139 weston_env = {}
140 weston_ini = []
141 ld_library_paths = []
143 if weston_root_dir is None:
144 # There is very little to do, simply write a configuration
145 # that removes the panel to give some extra screen real estate.
146 weston_ini.extend([
147 "[shell]",
148 "background-color=0x00000000",
149 "panel-position=none",
150 # Don't look for a background image.
151 "background-image=",
153 else:
154 weston_ini.extend([
155 "[core]",
157 "[shell]",
158 "background-color=0x00000000",
159 "client={:s}/libexec/weston-desktop-shell".format(weston_root_dir),
160 "panel-position=none",
161 # Don't look for a background image.
162 "background-image=",
164 "[keyboard]",
165 "numlock-on=true",
167 "[output]",
168 "seat=default",
170 "[input-method]",
171 "path={:s}/libexec/weston-keyboard".format(weston_root_dir),
174 if wayland_root_dir is not None:
175 ld_library_paths.append(os.path.join(wayland_root_dir, "lib64"))
177 if weston_root_dir is not None:
178 weston_lib_dir = os.path.join(weston_root_dir, "lib")
179 ld_library_paths.extend([
180 weston_lib_dir,
181 os.path.join(weston_lib_dir, "weston"),
184 # Setup the `WESTON_MODULE_MAP`.
185 weston_map_filenames = {
186 "wayland-backend.so": "",
187 "gl-renderer.so": "",
188 "headless-backend.so": "",
189 "desktop-shell.so": "",
192 for entry in scantree(weston_lib_dir):
193 if entry.name in weston_map_filenames:
194 weston_map_filenames[entry.name] = os.path.normpath(entry.path)
196 module_map = []
197 for key, value in sorted(weston_map_filenames.items()):
198 if not value:
199 raise Exception("Failure to find {!r} in {!r}".format(key, weston_lib_dir))
200 module_map.append("{:s}={:s}".format(key, value))
202 weston_env["WESTON_MODULE_MAP"] = ";".join(module_map)
203 del module_map
205 if ld_library_paths:
206 ld_library_paths_str = os.environ.get("LD_LIBRARY_PATH", "")
207 if ld_library_paths_str:
208 ld_library_paths.insert(0, ld_library_paths_str.rstrip(":"))
209 weston_env["LD_LIBRARY_PATH"] = ":".join(ld_library_paths)
210 del ld_library_paths_str
212 return (
213 {**os.environ, **weston_env} if weston_env else None,
214 "\n".join(weston_ini),
217 @staticmethod
218 def _weston_env_and_ini_from_system() -> tuple[dict[str, str] | None, str]:
219 weston_env = None
220 weston_ini = [
221 "[shell]",
222 "background-color=0x00000000",
223 "panel-position=none",
224 # Don't look for a background image.
225 "background-image=",
227 return (
228 weston_env,
229 "\n".join(weston_ini),
232 @staticmethod
233 def _weston_env_and_ini() -> tuple[dict[str, str] | None, str]:
234 wayland_root_dir = os.environ.get("WAYLAND_ROOT_DIR")
235 weston_root_dir = os.environ.get("WESTON_ROOT_DIR")
237 if wayland_root_dir or weston_root_dir:
238 weston_env, weston_ini = backend_wayland._weston_env_and_ini_from_portable(
239 wayland_root_dir=wayland_root_dir,
240 weston_root_dir=weston_root_dir,
242 else:
243 weston_env, weston_ini = backend_wayland._weston_env_and_ini_from_system()
244 return weston_env, weston_ini
246 @staticmethod
247 def run(blender_args: Sequence[str]) -> int:
248 # Use the PID to support running multiple tests at once.
249 socket = "wl-blender-{:d}".format(os.getpid())
251 weston_bin = os.environ.get("WESTON_BIN", "weston")
253 # Ensure the WAYLAND server is NOT running (for this socket).
254 if backend_wayland._wait_for_wayland_server(socket=socket, timeout=0.0):
255 sys.stderr.write("Wayland server for socket \"{:s}\" already running, exiting!\n".format(socket))
256 return 1
258 weston_env, weston_ini = backend_wayland._weston_env_and_ini()
260 cmd = [
261 weston_bin,
262 "--socket={:s}".format(socket),
263 *(() if USE_WINDOW else ("--backend=headless",)),
264 "--width=800",
265 "--height=600",
266 # `--config={..}` is added to point to a temp file.
268 cmd_kw: dict[str, Any] = {}
269 if weston_env is not None:
270 cmd_kw["env"] = weston_env
271 if not VERBOSE:
272 cmd_kw["stderr"] = subprocess.PIPE
273 cmd_kw["stdout"] = subprocess.PIPE
275 if VERBOSE:
276 print("Env:", weston_env)
277 print("Run:", cmd)
279 with tempfile.NamedTemporaryFile(
280 prefix="weston_",
281 suffix=".ini",
282 mode='w',
283 encoding="utf-8",
284 ) as weston_ini_tempfile:
285 weston_ini_tempfile.write(weston_ini)
286 weston_ini_tempfile.flush()
287 with subprocess.Popen(
288 [*cmd, "--config={:s}".format(weston_ini_tempfile.name)],
289 **cmd_kw,
290 ) as proc_server:
291 del cmd, cmd_kw
292 if not backend_wayland._wait_for_wayland_server(socket=socket, timeout=1.0):
293 # The verbose mode will have written to standard out/error already.
294 # Only show the output is the server wasn't able to start.
295 if not VERBOSE:
296 assert proc_server.stdout is not None
297 assert proc_server.stderr is not None
298 sys.stderr.write("Unable to start wayland server, exiting!\n")
299 sys.stderr.write(proc_server.stdout.read().decode("utf-8", errors="surrogateescape"))
300 sys.stderr.write(proc_server.stderr.read().decode("utf-8", errors="surrogateescape"))
301 sys.stderr.write("\n")
302 proc_server.send_signal(signal.SIGINT)
303 # Wait for the interrupt to be handled.
304 proc_server.communicate()
305 return 1
307 blender_env = {**os.environ, "WAYLAND_DISPLAY": socket}
309 # Needed so Blender can find WAYLAND libraries such as `libwayland-cursor.so`.
310 if weston_env is not None and "LD_LIBRARY_PATH" in weston_env:
311 blender_env["LD_LIBRARY_PATH"] = weston_env["LD_LIBRARY_PATH"]
313 cmd = [
314 # "strace", # Can be useful for debugging any startup issues.
315 BLENDER_BIN,
316 *blender_args,
319 if USE_DEBUG:
320 cmd = ["gdb", BLENDER_BIN, "--ex=run", "--args", *cmd]
322 if VERBOSE:
323 print("Env:", blender_env)
324 print("Run:", cmd)
325 with subprocess.Popen(cmd, env=blender_env) as proc_blender:
326 proc_blender.communicate()
327 blender_exit_code = proc_blender.returncode
328 del cmd
330 # Blender has finished, close the server.
331 proc_server.send_signal(signal.SIGINT)
332 # Wait for the interrupt to be handled.
333 proc_server.communicate()
335 # Forward Blender's exit code.
336 return blender_exit_code
339 # -----------------------------------------------------------------------------
340 # Main Function
342 def main() -> int:
343 match sys.platform:
344 case "darwin":
345 backend = backend_base
346 case "win32":
347 backend = backend_base
348 case _:
349 backend = backend_wayland
350 return backend.run(sys.argv[1:])
353 if __name__ == "__main__":
354 sys.exit(main())