15 from functools import lru_cache
16 from pathlib import PurePath, Path
18 from typing import Any
20 from scripts.common import get_meson
21 from scripts.common import git
22 from scripts.common import win32_get_short_path_name
23 from scripts.common import get_wine_shortpath
25 SCRIPTDIR = os.path.dirname(os.path.realpath(__file__))
26 PREFIX_DIR = os.path.join(SCRIPTDIR, 'prefix')
27 # Look for the following build dirs: `build` `_build` `builddir`
28 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'build')
29 if not os.path.exists(DEFAULT_BUILDDIR):
30 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, '_build')
31 if not os.path.exists(DEFAULT_BUILDDIR):
32 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'builddir')
34 TYPELIB_REG = re.compile(r'.*\.typelib$')
35 SHAREDLIB_REG = re.compile(r'\.so|\.dylib|\.dll')
37 # libdir is expanded from option of the same name listed in the `meson
38 # introspect --buildoptions` output.
39 GSTPLUGIN_FILEPATH_REG_TEMPLATE = r'.*/{libdir}/gstreamer-1.0/[^/]+$'
40 GSTPLUGIN_FILEPATH_REG = None
43 BASH_COMPLETION_SCRIPTS="{bash_completions}"
44 BASH_COMPLETION_PATHS="{bash_completions_paths}"
45 for p in $BASH_COMPLETION_PATHS; do
46 for f in $BASH_COMPLETION_SCRIPTS; do
47 [ -f "$p/$f" ] && . "$p/$f"
51 BASH_COMPLETION_PATHS = [SCRIPTDIR + '/subprojects/gstreamer/data/bash-completion/completions']
52 BASH_COMPLETION_PATHS += [SCRIPTDIR + '/subprojects/gst-devtools/validate/data/bash-completion/completions']
55 def str_to_bool(value: Any) -> bool:
56 """Return whether the provided string (or any value really) represents true. Otherwise false.
57 Just like plugin server stringToBoolean.
61 return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
65 if isinstance(o, str):
67 if isinstance(o, list):
69 raise AssertionError('Object {!r} must be a string or a list'.format(o))
73 if isinstance(o, str):
75 if isinstance(o, list):
78 raise AssertionError('Did not expect object {!r} to have more than one element'.format(o))
79 raise AssertionError('Object {!r} must be a string or a list'.format(o))
82 def prepend_env_var(env, var, value, sysroot):
85 if value.startswith(sysroot):
86 value = value[len(sysroot):]
87 # Try not to exceed maximum length limits for env vars on Windows
89 value = win32_get_short_path_name(value)
90 env_val = env.get(var, '')
91 val = os.pathsep + value + os.pathsep
92 # Don't add the same value twice
93 if val in env_val or env_val.startswith(value + os.pathsep):
95 env[var] = val + env_val
96 env[var] = env[var].replace(os.pathsep + os.pathsep, os.pathsep).strip(os.pathsep)
99 def get_target_install_filename(target, filename):
101 Checks whether this file is one of the files installed by the target
103 basename = os.path.basename(filename)
104 for install_filename in listify(target['install_filename']):
105 if install_filename.endswith(basename):
106 return install_filename
110 def get_pkgconfig_variable_from_pcfile(pcfile, varname):
112 substre = re.compile('\$\{[^${}]+\}')
113 with pcfile.open('r', encoding='utf-8') as f:
117 key, value = line[:-1].split('=', 1)
119 for each in substre.findall(value):
120 substkey = each[2:-1]
121 subst[each] = variables.get(substkey, '')
122 for k, v in subst.items():
123 value = value.replace(k, v)
124 variables[key] = value
125 return variables.get(varname, '')
129 def get_pkgconfig_variable(builddir, pcname, varname):
131 Parsing isn't perfect, but it's good enough.
133 pcfile = Path(builddir) / 'meson-private' / (pcname + '.pc')
135 return get_pkgconfig_variable_from_pcfile(pcfile, varname)
136 return subprocess.check_output(['pkg-config', pcname, '--variable=' + varname],
137 universal_newlines=True, encoding='utf-8')
140 def is_gio_module(target, filename, builddir):
141 if target['type'] != 'shared module':
143 install_filename = get_target_install_filename(target, filename)
144 if not install_filename:
146 giomoduledir = PurePath(get_pkgconfig_variable(builddir, 'gio-2.0', 'giomoduledir'))
147 fpath = PurePath(install_filename)
148 if fpath.parent != giomoduledir:
153 def is_library_target_and_not_plugin(target, filename):
155 Don't add plugins to PATH/LD_LIBRARY_PATH because:
157 2. It causes us to exceed the PATH length limit on Windows and Wine
159 if target['type'] != 'shared library':
161 # Check if this output of that target is a shared library
162 if not SHAREDLIB_REG.search(filename):
164 # Check if it's installed to the gstreamer plugin location
165 install_filename = get_target_install_filename(target, filename)
166 if not install_filename:
168 global GSTPLUGIN_FILEPATH_REG
169 if GSTPLUGIN_FILEPATH_REG is None:
170 GSTPLUGIN_FILEPATH_REG = re.compile(GSTPLUGIN_FILEPATH_REG_TEMPLATE)
171 if GSTPLUGIN_FILEPATH_REG.search(install_filename.replace('\\', '/')):
176 def is_binary_target_and_in_path(target, filename, bindir):
177 if target['type'] != 'executable':
179 # Check if this file installed by this target is installed to bindir
180 install_filename = get_target_install_filename(target, filename)
181 if not install_filename:
183 fpath = PurePath(install_filename)
184 if fpath.parent != bindir:
189 def get_wine_subprocess_env(options, env):
190 with open(os.path.join(options.builddir, 'meson-info', 'intro-buildoptions.json')) as f:
191 buildoptions = json.load(f)
193 prefix, = [o for o in buildoptions if o['name'] == 'prefix']
194 path = os.path.normpath(os.path.join(prefix['value'], 'bin'))
195 prepend_env_var(env, "PATH", path, options.sysroot)
196 wine_path = get_wine_shortpath(
197 options.wine.split(' '),
198 [path] + env.get('WINEPATH', '').split(';')
201 wine_path += ';' + options.winepath
202 env['WINEPATH'] = wine_path
203 env['WINEDEBUG'] = 'fixme-all'
208 def setup_gdb(options):
211 if not shutil.which('gdb'):
214 bdir = pathlib.Path(options.builddir).resolve()
215 for libpath, gdb_path in [
216 (os.path.join("subprojects", "gstreamer", "gst"),
217 os.path.join("subprojects", "gstreamer", "libs", "gst", "helpers")),
218 (os.path.join("subprojects", "glib", "gobject"), None),
219 (os.path.join("subprojects", "glib", "glib"), None)]:
224 autoload_path = (pathlib.Path(bdir) / 'gdb-auto-load').joinpath(*bdir.parts[1:]) / libpath
225 autoload_path.mkdir(parents=True, exist_ok=True)
226 for gdb_helper in glob.glob(str(bdir / gdb_path / "*-gdb.py")):
227 python_paths.add(str(bdir / gdb_path))
228 python_paths.add(os.path.join(options.srcdir, gdb_path))
231 shutil.copy(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
233 os.symlink(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
234 except (FileExistsError, shutil.SameFileError):
237 gdbinit_line = 'add-auto-load-scripts-directory {}\n'.format(bdir / 'gdb-auto-load')
239 with open(os.path.join(options.srcdir, '.gdbinit'), 'r') as f:
240 if gdbinit_line in f.readlines():
242 except FileNotFoundError:
245 with open(os.path.join(options.srcdir, '.gdbinit'), 'a') as f:
246 f.write(gdbinit_line)
251 def is_bash_completion_available(options):
252 return os.path.exists(os.path.join(options.builddir, 'subprojects/gstreamer/data/bash-completion/helpers/gst'))
255 def get_subprocess_env(options, gst_version):
256 env = os.environ.copy()
258 env["CURRENT_GST"] = os.path.normpath(SCRIPTDIR)
259 env["GST_VERSION"] = gst_version
260 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath(
261 "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR),
263 env["GST_VALIDATE_PLUGIN_PATH"] = os.path.normpath(
264 "%s/subprojects/gst-devtools/validate/plugins" % options.builddir)
265 prepend_env_var(env, "GST_VALIDATE_APPS_DIR", os.path.normpath(
266 "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR),
268 env["GST_ENV"] = 'gst-' + gst_version
269 env["GST_REGISTRY"] = os.path.normpath(options.builddir + "/registry.dat")
270 prepend_env_var(env, "PATH", os.path.normpath(
271 "%s/subprojects/gst-devtools/validate/tools" % options.builddir),
274 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath(
275 "%s/subprojects/gst-examples/webrtc/check/validate/scenarios" %
276 SCRIPTDIR), options.sysroot)
277 prepend_env_var(env, "GST_VALIDATE_APPS_DIR", os.path.normpath(
278 "%s/subprojects/gst-examples/webrtc/check/validate/apps" %
279 SCRIPTDIR), options.sysroot)
282 return get_wine_subprocess_env(options, env)
284 prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'meson'),
287 env["GST_PLUGIN_SYSTEM_PATH"] = ""
288 env["GST_PLUGIN_SCANNER"] = os.path.normpath(
289 "%s/subprojects/gstreamer/libs/gst/helpers/gst-plugin-scanner" % options.builddir)
290 env["GST_PTP_HELPER"] = os.path.normpath(
291 "%s/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper" % options.builddir)
294 lib_path_envvar = 'PATH'
295 elif platform.system() == 'Darwin':
296 # RPATH is sufficient on macOS, and DYLD_LIBRARY_PATH can cause issues with dynamic linker path priority
297 lib_path_envvar = None
299 lib_path_envvar = 'LD_LIBRARY_PATH'
301 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(SCRIPTDIR, 'subprojects',
302 'gst-python', 'plugin'),
304 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(PREFIX_DIR, 'lib',
307 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(options.builddir, 'subprojects',
310 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH",
311 os.path.join(PREFIX_DIR, 'share', 'gstreamer-1.0',
312 'validate', 'scenarios'),
314 prepend_env_var(env, "GI_TYPELIB_PATH", os.path.join(PREFIX_DIR, 'lib',
315 'lib', 'girepository-1.0'),
317 prepend_env_var(env, "PKG_CONFIG_PATH", os.path.join(PREFIX_DIR, 'lib', 'pkgconfig'),
321 prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'scripts'),
324 # tools: gst-launch-1.0, gst-inspect-1.0
325 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
326 'gstreamer', 'tools'),
328 # plugin scanner and generator
329 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
330 'gstreamer', 'docs'),
332 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
333 'gst-plugins-base', 'tools'),
336 # Library and binary search paths
337 prepend_env_var(env, "PATH", os.path.join(PREFIX_DIR, 'bin'),
339 if lib_path_envvar != 'PATH':
340 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib'),
342 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib64'),
344 elif 'QMAKE' in os.environ:
345 # There's no RPATH on Windows, so we need to set PATH for the qt5 DLLs
346 prepend_env_var(env, 'PATH', os.path.dirname(os.environ['QMAKE']),
350 targets_s = subprocess.check_output(meson + ['introspect', options.builddir, '--targets'])
351 targets = json.loads(targets_s.decode())
354 srcdir_path = pathlib.Path(options.srcdir)
356 build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions'])
357 build_options = json.loads(build_options_s.decode())
358 libdir, = [o['value'] for o in build_options if o['name'] == 'libdir']
359 libdir = PurePath(libdir)
360 prefix, = [o['value'] for o in build_options if o['name'] == 'prefix']
361 bindir, = [o['value'] for o in build_options if o['name'] == 'bindir']
362 prefix = PurePath(prefix)
363 bindir = prefix / bindir
365 global GSTPLUGIN_FILEPATH_REG_TEMPLATE
366 GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix())
368 for target in targets:
369 filenames = listify(target['filename'])
370 if not target['installed']:
372 for filename in filenames:
373 root = os.path.dirname(filename)
374 if srcdir_path / "subprojects/gst-devtools/validate/plugins" in (srcdir_path / root).parents:
376 if filename.endswith('.dll'):
377 mono_paths.add(os.path.join(options.builddir, root))
378 if TYPELIB_REG.search(filename):
379 prepend_env_var(env, "GI_TYPELIB_PATH",
380 os.path.join(options.builddir, root),
382 elif is_library_target_and_not_plugin(target, filename):
383 prepend_env_var(env, lib_path_envvar,
384 os.path.join(options.builddir, root),
386 elif is_binary_target_and_in_path(target, filename, bindir):
387 paths.add(os.path.join(options.builddir, root))
388 elif is_gio_module(target, filename, options.builddir):
389 prepend_env_var(env, 'GIO_EXTRA_MODULES',
390 os.path.join(options.builddir, root),
393 with open(os.path.join(options.builddir, 'GstPluginsPath.json')) as f:
394 for plugin_path in json.load(f):
395 prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path,
398 # Sort to iterate in a consistent order (`set`s and `hash`es are randomized)
399 for p in sorted(paths):
400 prepend_env_var(env, 'PATH', p, options.sysroot)
403 for p in sorted(mono_paths):
404 prepend_env_var(env, "MONO_PATH", p, options.sysroot)
407 encoding_targets = set()
408 python_dirs = setup_gdb(options)
409 overrides_dirs = set()
410 if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode():
411 installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed'])
412 for path, installpath in json.loads(installed_s.decode()).items():
413 installpath_parts = pathlib.Path(installpath).parts
415 # We want to add all python modules to the PYTHONPATH
416 # in a manner consistent with the way they would be imported:
417 # For example if the source path /home/meh/foo/bar.py
418 # is to be installed in /usr/lib/python/site-packages/foo/bar.py,
419 # we want to add /home/meh to the PYTHONPATH.
420 # This will only work for projects where the paths to be installed
421 # mirror the installed directory layout, for example if the path
422 # is /home/meh/baz/bar.py and the install path is
423 # /usr/lib/site-packages/foo/bar.py , we will not add anything
424 # to PYTHONPATH, but the current approach works with pygobject
425 # and gst-python at least.
426 if 'site-packages' in installpath_parts:
427 install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:])
428 if path.endswith(install_subpath):
429 if os.path.commonprefix(["gi/overrides", install_subpath]):
430 overrides_dirs.add(os.path.dirname(path))
432 python_dirs.add(path[:len(install_subpath) * -1])
434 if path.endswith('.prs'):
435 presets.add(os.path.dirname(path))
436 elif path.endswith('.gep'):
437 encoding_targets.add(
438 os.path.abspath(os.path.join(os.path.dirname(path), '..')))
440 if path.endswith('gstomx.conf'):
441 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
444 for p in sorted(presets):
445 prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
447 for t in sorted(encoding_targets):
448 prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
450 # Check if meson has generated -uninstalled pkgconfig files
451 meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
452 if meson_uninstalled.is_dir():
453 prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
455 for python_dir in sorted(python_dirs):
456 prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
458 for python_dir in sorted(overrides_dirs):
459 prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot)
461 mesonpath = os.path.join(SCRIPTDIR, "meson")
462 if os.path.join(mesonpath):
463 # Add meson/ into PYTHONPATH if we are using a local meson
464 prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
466 # Ensure that gst-python/gi is used first
467 prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'),
471 if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
472 # Preserve default paths when empty
473 prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
475 prepend_env_var(env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
481 if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
482 # Preserve default paths when empty
483 prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
485 prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
491 def get_windows_shell():
492 command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file',
493 os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')]
494 result = subprocess.check_output(command)
495 return result.decode().strip()
498 if __name__ == "__main__":
499 parser = argparse.ArgumentParser(prog="gst-env")
501 parser.add_argument("--builddir",
502 default=DEFAULT_BUILDDIR,
503 help="The meson build directory")
504 parser.add_argument("--srcdir",
506 help="The top level source directory")
507 parser.add_argument("--sysroot",
509 help="The sysroot path used during cross-compilation")
510 parser.add_argument("--wine",
512 help="Build a wine env based on specified wine command")
513 parser.add_argument("--winepath",
515 help="Extra path to set to WINEPATH.")
516 parser.add_argument("--only-environment",
519 help="Do not start a shell, only print required environment.")
520 options, args = parser.parse_known_args()
522 if not os.path.exists(options.builddir):
523 print("GStreamer not built in %s\n\nBuild it and try again" %
526 options.builddir = os.path.abspath(options.builddir)
528 if not os.path.exists(options.srcdir):
529 print("The specified source dir does not exist" %
533 # The following incantation will retrieve the current branch name.
535 gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
536 repository_path=options.srcdir).strip('\n')
537 except subprocess.CalledProcessError:
538 gst_version = "unknown"
541 gst_version += '-' + os.path.basename(options.wine)
543 env = get_subprocess_env(options, gst_version)
546 shell = get_windows_shell()
547 if shell == 'powershell.exe':
548 args = ['powershell.exe']
549 args += ['-NoLogo', '-NoExit']
550 prompt = 'function global:prompt { "[gst-' + gst_version + '"+"] PS " + $PWD + "> "}'
551 args += ['-Command', prompt]
553 args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
554 args += ['/k', 'prompt [gst-{}] $P$G'.format(gst_version)]
556 args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
557 if args[0].endswith('bash') and not str_to_bool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
558 # Let the GC remove the tmp file
559 tmprc = tempfile.NamedTemporaryFile(mode='w')
560 bashrc = os.path.expanduser('~/.bashrc')
561 if os.path.exists(bashrc):
562 with open(bashrc, 'r') as src:
563 shutil.copyfileobj(src, tmprc)
564 tmprc.write('\nexport PS1="[gst-%s] $PS1"' % gst_version)
566 if is_bash_completion_available(options):
567 bash_completions_files = []
568 for p in BASH_COMPLETION_PATHS:
569 if os.path.exists(p):
570 bash_completions_files += os.listdir(path=p)
571 bc_rc = BC_RC.format(bash_completions=' '.join(bash_completions_files), bash_completions_paths=' '.join(BASH_COMPLETION_PATHS))
574 args.append("--rcfile")
575 args.append(tmprc.name)
576 elif args[0].endswith('fish'):
577 # Ignore SIGINT while using fish as the shell to make it behave
578 # like other shells such as bash and zsh.
579 # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
580 signal.signal(signal.SIGINT, lambda x, y: True)
582 args.append('--init-command')
583 prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
585 echo -n '[gst-{}] '(original_fish_prompt)
586 end'''.format(gst_version)
587 args.append(prompt_cmd)
588 elif args[0].endswith('zsh'):
589 tmpdir = tempfile.TemporaryDirectory()
590 # Let the GC remove the tmp file
591 tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w')
592 zshrc = os.path.expanduser('~/.zshrc')
593 if os.path.exists(zshrc):
594 with open(zshrc, 'r') as src:
595 shutil.copyfileobj(src, tmprc)
596 tmprc.write('\nexport PROMPT="[gst-{}] $PROMPT"'.format(gst_version))
598 env['ZDOTDIR'] = tmpdir.name
600 if options.only_environment:
601 for name, value in env.items():
602 print('{}={}'.format(name, shlex.quote(value)))
603 print('export {}'.format(name))
605 if os.environ.get("CI_PROJECT_NAME"):
606 print("Ignoring SIGINT when running on the CI,"
607 " as we get spurious sigint in there for some reason.")
608 signal.signal(signal.SIGINT, signal.SIG_IGN)
609 exit(subprocess.call(args, close_fds=False, env=env))
611 except subprocess.CalledProcessError as e: