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 # Search for the Plugin paths file either in the build directory root
394 # or check if gstreamer is a subproject of another project
395 for sub_directories in [[], ['subprojects', 'gstreamer']]:
396 plugin_paths = os.path.join(options.builddir, *sub_directories, 'GstPluginsPath.json')
397 if os.path.exists(plugin_paths):
398 with open(plugin_paths) as f:
399 for plugin_path in json.load(f):
400 prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path,
404 # Sort to iterate in a consistent order (`set`s and `hash`es are randomized)
405 for p in sorted(paths):
406 prepend_env_var(env, 'PATH', p, options.sysroot)
409 for p in sorted(mono_paths):
410 prepend_env_var(env, "MONO_PATH", p, options.sysroot)
413 encoding_targets = set()
414 python_dirs = setup_gdb(options)
415 overrides_dirs = set()
416 if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode():
417 installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed'])
418 for path, installpath in json.loads(installed_s.decode()).items():
419 installpath_parts = pathlib.Path(installpath).parts
421 # We want to add all python modules to the PYTHONPATH
422 # in a manner consistent with the way they would be imported:
423 # For example if the source path /home/meh/foo/bar.py
424 # is to be installed in /usr/lib/python/site-packages/foo/bar.py,
425 # we want to add /home/meh to the PYTHONPATH.
426 # This will only work for projects where the paths to be installed
427 # mirror the installed directory layout, for example if the path
428 # is /home/meh/baz/bar.py and the install path is
429 # /usr/lib/site-packages/foo/bar.py , we will not add anything
430 # to PYTHONPATH, but the current approach works with pygobject
431 # and gst-python at least.
432 if 'site-packages' in installpath_parts:
433 install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:])
434 if path.endswith(install_subpath):
435 if os.path.commonprefix(["gi/overrides", install_subpath]):
436 overrides_dirs.add(os.path.dirname(path))
438 python_dirs.add(path[:len(install_subpath) * -1])
440 if path.endswith('.prs'):
441 presets.add(os.path.dirname(path))
442 elif path.endswith('.gep'):
443 encoding_targets.add(
444 os.path.abspath(os.path.join(os.path.dirname(path), '..')))
446 if path.endswith('gstomx.conf'):
447 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
450 for p in sorted(presets):
451 prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
453 for t in sorted(encoding_targets):
454 prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
456 # Check if meson has generated -uninstalled pkgconfig files
457 meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
458 if meson_uninstalled.is_dir():
459 prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
461 for python_dir in sorted(python_dirs):
462 prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
464 for python_dir in sorted(overrides_dirs):
465 prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot)
467 mesonpath = os.path.join(SCRIPTDIR, "meson")
468 if os.path.join(mesonpath):
469 # Add meson/ into PYTHONPATH if we are using a local meson
470 prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
472 # Ensure that gst-python/gi is used first
473 prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'),
477 if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
478 # Preserve default paths when empty
479 prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
481 prepend_env_var(env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
487 if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
488 # Preserve default paths when empty
489 prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
491 prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
497 def get_windows_shell():
498 command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file',
499 os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')]
500 result = subprocess.check_output(command)
501 return result.decode().strip()
504 if __name__ == "__main__":
505 parser = argparse.ArgumentParser(prog="gst-env")
507 parser.add_argument("--builddir",
508 default=DEFAULT_BUILDDIR,
509 help="The meson build directory")
510 parser.add_argument("--srcdir",
512 help="The top level source directory")
513 parser.add_argument("--sysroot",
515 help="The sysroot path used during cross-compilation")
516 parser.add_argument("--wine",
518 help="Build a wine env based on specified wine command")
519 parser.add_argument("--winepath",
521 help="Extra path to set to WINEPATH.")
522 parser.add_argument("--only-environment",
525 help="Do not start a shell, only print required environment.")
526 options, args = parser.parse_known_args()
528 if not os.path.exists(options.builddir):
529 print("GStreamer not built in %s\n\nBuild it and try again" %
532 options.builddir = os.path.abspath(options.builddir)
534 if not os.path.exists(options.srcdir):
535 print("The specified source dir does not exist" %
539 # The following incantation will retrieve the current branch name.
541 gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
542 repository_path=options.srcdir).strip('\n')
543 except subprocess.CalledProcessError:
544 gst_version = "unknown"
547 gst_version += '-' + os.path.basename(options.wine)
549 env = get_subprocess_env(options, gst_version)
552 shell = get_windows_shell()
553 if shell == 'powershell.exe':
554 args = ['powershell.exe']
555 args += ['-NoLogo', '-NoExit']
556 prompt = 'function global:prompt { "[gst-' + gst_version + '"+"] PS " + $PWD + "> "}'
557 args += ['-Command', prompt]
559 args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
560 args += ['/k', 'prompt [gst-{}] $P$G'.format(gst_version)]
562 args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
563 if args[0].endswith('bash') and not str_to_bool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
564 # Let the GC remove the tmp file
565 tmprc = tempfile.NamedTemporaryFile(mode='w')
566 bashrc = os.path.expanduser('~/.bashrc')
567 if os.path.exists(bashrc):
568 with open(bashrc, 'r') as src:
569 shutil.copyfileobj(src, tmprc)
570 tmprc.write('\nexport PS1="[gst-%s] $PS1"' % gst_version)
572 if is_bash_completion_available(options):
573 bash_completions_files = []
574 for p in BASH_COMPLETION_PATHS:
575 if os.path.exists(p):
576 bash_completions_files += os.listdir(path=p)
577 bc_rc = BC_RC.format(bash_completions=' '.join(bash_completions_files), bash_completions_paths=' '.join(BASH_COMPLETION_PATHS))
580 args.append("--rcfile")
581 args.append(tmprc.name)
582 elif args[0].endswith('fish'):
583 # Ignore SIGINT while using fish as the shell to make it behave
584 # like other shells such as bash and zsh.
585 # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
586 signal.signal(signal.SIGINT, lambda x, y: True)
588 args.append('--init-command')
589 prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
591 echo -n '[gst-{}] '(original_fish_prompt)
592 end'''.format(gst_version)
593 args.append(prompt_cmd)
594 elif args[0].endswith('zsh'):
595 tmpdir = tempfile.TemporaryDirectory()
596 # Let the GC remove the tmp file
597 tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w')
598 zshrc = os.path.expanduser('~/.zshrc')
599 if os.path.exists(zshrc):
600 with open(zshrc, 'r') as src:
601 shutil.copyfileobj(src, tmprc)
602 tmprc.write('\nexport PROMPT="[gst-{}] $PROMPT"'.format(gst_version))
604 env['ZDOTDIR'] = tmpdir.name
606 if options.only_environment:
607 for name, value in env.items():
608 print('{}={}'.format(name, shlex.quote(value)))
609 print('export {}'.format(name))
611 if os.environ.get("CI_PROJECT_NAME"):
612 print("Ignoring SIGINT when running on the CI,"
613 " as we get spurious sigint in there for some reason.")
614 signal.signal(signal.SIGINT, signal.SIG_IGN)
615 exit(subprocess.call(args, close_fds=False, env=env))
617 except subprocess.CalledProcessError as e: