15 from functools import lru_cache
16 from pathlib import PurePath, Path
19 from typing import Any
21 from scripts.common import get_meson
22 from scripts.common import git
23 from scripts.common import win32_get_short_path_name
24 from scripts.common import get_wine_shortpath
26 SCRIPTDIR = os.path.dirname(os.path.realpath(__file__))
27 PREFIX_DIR = os.path.join(SCRIPTDIR, 'prefix')
28 # Look for the following build dirs: `build` `_build` `builddir`
29 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'build')
30 if not os.path.exists(DEFAULT_BUILDDIR):
31 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, '_build')
32 if not os.path.exists(DEFAULT_BUILDDIR):
33 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'builddir')
35 TYPELIB_REG = re.compile(r'.*\.typelib$')
36 SHAREDLIB_REG = re.compile(r'\.so|\.dylib|\.dll')
38 # libdir is expanded from option of the same name listed in the `meson
39 # introspect --buildoptions` output.
40 GSTPLUGIN_FILEPATH_REG_TEMPLATE = r'.*/{libdir}/gstreamer-1.0/[^/]+$'
41 GSTPLUGIN_FILEPATH_REG = None
44 BASH_COMPLETION_SCRIPTS="{bash_completions}"
45 BASH_COMPLETION_PATHS="{bash_completions_paths}"
46 for p in $BASH_COMPLETION_PATHS; do
47 for f in $BASH_COMPLETION_SCRIPTS; do
48 [ -f "$p/$f" ] && . "$p/$f"
52 BASH_COMPLETION_PATHS = [SCRIPTDIR + '/subprojects/gstreamer/data/bash-completion/completions']
53 BASH_COMPLETION_PATHS += [SCRIPTDIR + '/subprojects/gst-devtools/validate/data/bash-completion/completions']
56 def str_to_bool(value: Any) -> bool:
57 """Return whether the provided string (or any value really) represents true. Otherwise false.
58 Just like plugin server stringToBoolean.
62 return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
66 if isinstance(o, str):
68 if isinstance(o, list):
70 raise AssertionError('Object {!r} must be a string or a list'.format(o))
74 if isinstance(o, str):
76 if isinstance(o, list):
79 raise AssertionError('Did not expect object {!r} to have more than one element'.format(o))
80 raise AssertionError('Object {!r} must be a string or a list'.format(o))
83 def prepend_env_var(env, var, value, sysroot):
86 if value.startswith(sysroot):
87 value = value[len(sysroot):]
88 # Try not to exceed maximum length limits for env vars on Windows
90 value = win32_get_short_path_name(value)
91 env_val = env.get(var, '')
92 val = os.pathsep + value + os.pathsep
93 # Don't add the same value twice
94 if val in env_val or env_val.startswith(value + os.pathsep):
96 env[var] = val + env_val
97 env[var] = env[var].replace(os.pathsep + os.pathsep, os.pathsep).strip(os.pathsep)
100 def get_target_install_filename(target, filename):
102 Checks whether this file is one of the files installed by the target
104 basename = os.path.basename(filename)
105 for install_filename in listify(target['install_filename']):
106 if install_filename.endswith(basename):
107 return install_filename
111 def get_pkgconfig_variable_from_pcfile(pcfile, varname):
113 substre = re.compile('\$\{[^${}]+\}')
114 with pcfile.open('r', encoding='utf-8') as f:
118 key, value = line[:-1].split('=', 1)
120 for each in substre.findall(value):
121 substkey = each[2:-1]
122 subst[each] = variables.get(substkey, '')
123 for k, v in subst.items():
124 value = value.replace(k, v)
125 variables[key] = value
126 return variables.get(varname, '')
130 def get_pkgconfig_variable(builddir, pcname, varname):
132 Parsing isn't perfect, but it's good enough.
134 pcfile = Path(builddir) / 'meson-private' / (pcname + '.pc')
136 return get_pkgconfig_variable_from_pcfile(pcfile, varname)
137 return subprocess.check_output(['pkg-config', pcname, '--variable=' + varname],
138 universal_newlines=True, encoding='utf-8')
141 def is_gio_module(target, filename, builddir):
142 if target['type'] != 'shared module':
144 install_filename = get_target_install_filename(target, filename)
145 if not install_filename:
147 giomoduledir = PurePath(get_pkgconfig_variable(builddir, 'gio-2.0', 'giomoduledir'))
148 fpath = PurePath(install_filename)
149 if fpath.parent != giomoduledir:
154 def is_library_target_and_not_plugin(target, filename):
156 Don't add plugins to PATH/LD_LIBRARY_PATH because:
158 2. It causes us to exceed the PATH length limit on Windows and Wine
160 if target['type'] != 'shared library':
162 # Check if this output of that target is a shared library
163 if not SHAREDLIB_REG.search(filename):
165 # Check if it's installed to the gstreamer plugin location
166 install_filename = get_target_install_filename(target, filename)
167 if not install_filename:
169 global GSTPLUGIN_FILEPATH_REG
170 if GSTPLUGIN_FILEPATH_REG is None:
171 GSTPLUGIN_FILEPATH_REG = re.compile(GSTPLUGIN_FILEPATH_REG_TEMPLATE)
172 if GSTPLUGIN_FILEPATH_REG.search(install_filename.replace('\\', '/')):
177 def is_binary_target_and_in_path(target, filename, bindir):
178 if target['type'] != 'executable':
180 # Check if this file installed by this target is installed to bindir
181 install_filename = get_target_install_filename(target, filename)
182 if not install_filename:
184 fpath = PurePath(install_filename)
185 if fpath.parent != bindir:
190 def get_wine_subprocess_env(options, env):
191 with open(os.path.join(options.builddir, 'meson-info', 'intro-buildoptions.json')) as f:
192 buildoptions = json.load(f)
194 prefix, = [o for o in buildoptions if o['name'] == 'prefix']
195 path = os.path.normpath(os.path.join(prefix['value'], 'bin'))
196 prepend_env_var(env, "PATH", path, options.sysroot)
197 wine_path = get_wine_shortpath(
198 options.wine.split(' '),
199 [path] + env.get('WINEPATH', '').split(';')
202 wine_path += ';' + options.winepath
203 env['WINEPATH'] = wine_path
204 env['WINEDEBUG'] = 'fixme-all'
209 def setup_gdb(options):
212 if not shutil.which('gdb'):
215 bdir = pathlib.Path(options.builddir).resolve()
216 for libpath, gdb_path in [
217 (os.path.join("subprojects", "gstreamer", "gst"),
218 os.path.join("subprojects", "gstreamer", "libs", "gst", "helpers")),
219 (os.path.join("subprojects", "glib", "gobject"), None),
220 (os.path.join("subprojects", "glib", "glib"), None)]:
225 autoload_path = (pathlib.Path(bdir) / 'gdb-auto-load').joinpath(*bdir.parts[1:]) / libpath
226 autoload_path.mkdir(parents=True, exist_ok=True)
227 for gdb_helper in glob.glob(str(bdir / gdb_path / "*-gdb.py")):
228 python_paths.add(str(bdir / gdb_path))
229 python_paths.add(os.path.join(options.srcdir, gdb_path))
232 shutil.copy(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
234 os.symlink(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
235 except (FileExistsError, shutil.SameFileError):
238 gdbinit_line = 'add-auto-load-scripts-directory {}\n'.format(bdir / 'gdb-auto-load')
240 with open(os.path.join(options.srcdir, '.gdbinit'), 'r') as f:
241 if gdbinit_line in f.readlines():
243 except FileNotFoundError:
246 with open(os.path.join(options.srcdir, '.gdbinit'), 'a') as f:
247 f.write(gdbinit_line)
252 def is_bash_completion_available(options):
253 return os.path.exists(os.path.join(options.builddir, 'subprojects/gstreamer/data/bash-completion/helpers/gst'))
256 def get_subprocess_env(options, gst_version):
257 env = os.environ.copy()
259 env["CURRENT_GST"] = os.path.normpath(SCRIPTDIR)
260 env["GST_VERSION"] = gst_version
261 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath(
262 "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR),
264 env["GST_VALIDATE_PLUGIN_PATH"] = os.path.normpath(
265 "%s/subprojects/gst-devtools/validate/plugins" % options.builddir)
266 prepend_env_var(env, "GST_VALIDATE_APPS_DIR", os.path.normpath(
267 "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR),
269 env["GST_ENV"] = gst_version
270 env["GST_REGISTRY"] = os.path.normpath(options.builddir + "/registry.dat")
271 prepend_env_var(env, "PATH", os.path.normpath(
272 "%s/subprojects/gst-devtools/validate/tools" % options.builddir),
275 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath(
276 "%s/subprojects/gst-examples/webrtc/check/validate/scenarios" %
277 SCRIPTDIR), options.sysroot)
278 prepend_env_var(env, "GST_VALIDATE_APPS_DIR", os.path.normpath(
279 "%s/subprojects/gst-examples/webrtc/check/validate/apps" %
280 SCRIPTDIR), options.sysroot)
283 return get_wine_subprocess_env(options, env)
285 prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'meson'),
288 env["GST_PLUGIN_SYSTEM_PATH"] = ""
289 env["GST_PLUGIN_SCANNER"] = os.path.normpath(
290 "%s/subprojects/gstreamer/libs/gst/helpers/gst-plugin-scanner" % options.builddir)
291 env["GST_PTP_HELPER"] = os.path.normpath(
292 "%s/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper" % options.builddir)
295 lib_path_envvar = 'PATH'
296 elif platform.system() == 'Darwin':
297 # RPATH is sufficient on macOS, and DYLD_LIBRARY_PATH can cause issues with dynamic linker path priority
298 lib_path_envvar = None
300 lib_path_envvar = 'LD_LIBRARY_PATH'
302 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(SCRIPTDIR, 'subprojects',
303 'gst-python', 'plugin'),
305 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(PREFIX_DIR, 'lib',
308 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(options.builddir, 'subprojects',
311 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH",
312 os.path.join(PREFIX_DIR, 'share', 'gstreamer-1.0',
313 'validate', 'scenarios'),
315 prepend_env_var(env, "GI_TYPELIB_PATH", os.path.join(PREFIX_DIR, 'lib',
316 'lib', 'girepository-1.0'),
318 prepend_env_var(env, "PKG_CONFIG_PATH", os.path.join(PREFIX_DIR, 'lib', 'pkgconfig'),
322 prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'scripts'),
325 # tools: gst-launch-1.0, gst-inspect-1.0
326 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
327 'gstreamer', 'tools'),
329 # plugin scanner and generator
330 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
331 'gstreamer', 'docs'),
333 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
334 'gst-plugins-base', 'tools'),
337 # Library and binary search paths
338 prepend_env_var(env, "PATH", os.path.join(PREFIX_DIR, 'bin'),
340 if lib_path_envvar != 'PATH':
341 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib'),
343 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib64'),
345 elif 'QMAKE' in os.environ:
346 # There's no RPATH on Windows, so we need to set PATH for the qt5 DLLs
347 prepend_env_var(env, 'PATH', os.path.dirname(os.environ['QMAKE']),
351 targets_s = subprocess.check_output(meson + ['introspect', options.builddir, '--targets'])
352 targets = json.loads(targets_s.decode())
355 srcdir_path = pathlib.Path(options.srcdir)
357 build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions'])
358 build_options = json.loads(build_options_s.decode())
359 libdir, = [o['value'] for o in build_options if o['name'] == 'libdir']
360 libdir = PurePath(libdir)
361 prefix, = [o['value'] for o in build_options if o['name'] == 'prefix']
362 bindir, = [o['value'] for o in build_options if o['name'] == 'bindir']
363 prefix = PurePath(prefix)
364 bindir = prefix / bindir
366 global GSTPLUGIN_FILEPATH_REG_TEMPLATE
367 GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix())
369 for target in targets:
370 filenames = listify(target['filename'])
371 if not target['installed']:
373 for filename in filenames:
374 root = os.path.dirname(filename)
375 if srcdir_path / "subprojects/gst-devtools/validate/plugins" in (srcdir_path / root).parents:
377 if filename.endswith('.dll'):
378 mono_paths.add(os.path.join(options.builddir, root))
379 if TYPELIB_REG.search(filename):
380 prepend_env_var(env, "GI_TYPELIB_PATH",
381 os.path.join(options.builddir, root),
383 elif is_library_target_and_not_plugin(target, filename):
384 prepend_env_var(env, lib_path_envvar,
385 os.path.join(options.builddir, root),
387 elif is_binary_target_and_in_path(target, filename, bindir):
388 paths.add(os.path.join(options.builddir, root))
389 elif is_gio_module(target, filename, options.builddir):
390 prepend_env_var(env, 'GIO_EXTRA_MODULES',
391 os.path.join(options.builddir, root),
394 # Search for the Plugin paths file either in the build directory root
395 # or check if gstreamer is a subproject of another project
396 for sub_directories in [[], ['subprojects', 'gstreamer']]:
397 plugin_paths = os.path.join(options.builddir, *sub_directories, 'GstPluginsPath.json')
398 if os.path.exists(plugin_paths):
399 with open(plugin_paths) as f:
400 for plugin_path in json.load(f):
401 prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path,
405 # Sort to iterate in a consistent order (`set`s and `hash`es are randomized)
406 for p in sorted(paths):
407 prepend_env_var(env, 'PATH', p, options.sysroot)
410 for p in sorted(mono_paths):
411 prepend_env_var(env, "MONO_PATH", p, options.sysroot)
414 encoding_targets = set()
415 python_dirs = setup_gdb(options)
416 overrides_dirs = set()
417 if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode():
418 installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed'])
419 for path, installpath in json.loads(installed_s.decode()).items():
420 installpath_parts = pathlib.Path(installpath).parts
422 # We want to add all python modules to the PYTHONPATH
423 # in a manner consistent with the way they would be imported:
424 # For example if the source path /home/meh/foo/bar.py
425 # is to be installed in /usr/lib/python/site-packages/foo/bar.py,
426 # we want to add /home/meh to the PYTHONPATH.
427 # This will only work for projects where the paths to be installed
428 # mirror the installed directory layout, for example if the path
429 # is /home/meh/baz/bar.py and the install path is
430 # /usr/lib/site-packages/foo/bar.py , we will not add anything
431 # to PYTHONPATH, but the current approach works with pygobject
432 # and gst-python at least.
434 if 'site-packages' in installpath_parts:
435 py_package = 'site-packages'
436 elif 'dist-packages' in installpath_parts:
437 py_package = 'dist-packages'
439 install_subpath = os.path.join(*installpath_parts[installpath_parts.index(py_package) + 1:])
440 if path.endswith(install_subpath):
441 if os.path.commonprefix(["gi/overrides", install_subpath]):
442 overrides_dirs.add(os.path.dirname(path))
444 python_dirs.add(path[:len(install_subpath) * -1])
446 if path.endswith('.prs'):
447 presets.add(os.path.dirname(path))
448 elif path.endswith('.gep'):
449 encoding_targets.add(
450 os.path.abspath(os.path.join(os.path.dirname(path), '..')))
452 if path.endswith('gstomx.conf'):
453 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
456 for p in sorted(presets):
457 prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
459 for t in sorted(encoding_targets):
460 prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
462 # Check if meson has generated -uninstalled pkgconfig files
463 meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
464 if meson_uninstalled.is_dir():
465 prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
467 for python_dir in sorted(python_dirs):
468 prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
470 for python_dir in sorted(overrides_dirs):
471 prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot)
473 mesonpath = os.path.join(SCRIPTDIR, "meson")
474 if os.path.join(mesonpath):
475 # Add meson/ into PYTHONPATH if we are using a local meson
476 prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
478 # Ensure that gst-python/gi is used first
479 prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'),
483 if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
484 # Preserve default paths when empty
485 prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
487 prepend_env_var(env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
493 if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
494 # Preserve default paths when empty
495 prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
497 prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
503 def get_windows_shell():
504 command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file',
505 os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')]
506 result = subprocess.check_output(command)
507 return result.decode().strip()
510 if __name__ == "__main__":
511 parser = argparse.ArgumentParser(prog="gst-env")
513 parser.add_argument("--builddir",
514 default=DEFAULT_BUILDDIR,
515 help="The meson build directory")
516 parser.add_argument("--srcdir",
518 help="The top level source directory")
519 parser.add_argument("--sysroot",
521 help="The sysroot path used during cross-compilation")
522 parser.add_argument("--wine",
524 help="Build a wine env based on specified wine command")
525 parser.add_argument("--winepath",
527 help="Extra path to set to WINEPATH.")
528 parser.add_argument("--only-environment",
531 help="Do not start a shell, only print required environment.")
532 options, args = parser.parse_known_args()
534 if not os.path.exists(options.builddir):
535 print("GStreamer not built in %s\n\nBuild it and try again" %
538 options.builddir = os.path.abspath(options.builddir)
540 if not os.path.exists(options.srcdir):
541 print("The specified source dir does not exist" %
545 # The following incantation will retrieve the current branch name.
547 gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
548 repository_path=options.srcdir).strip('\n')
549 except subprocess.CalledProcessError:
550 gst_version = "unknown"
553 gst_version += '-' + os.path.basename(options.wine)
555 env = get_subprocess_env(options, gst_version)
557 shell = get_windows_shell()
558 if shell in ['powershell.exe', 'pwsh.exe']:
559 new_args = [shell, '-NoLogo']
561 prompt = 'function global:prompt { "[' + gst_version + '"+"] PS " + $PWD + "> "}'
562 new_args += ['-NoExit', '-Command', prompt]
564 new_args += ['-NonInteractive', '-Command'] + args
567 new_args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
569 new_args += ['/k', 'prompt [{}] $P$G'.format(gst_version)]
571 new_args += ['/c', 'start', '/b', '/wait'] + args
575 args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
576 if args[0].endswith('bash') and not str_to_bool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
577 # Let the GC remove the tmp file
578 tmprc = tempfile.NamedTemporaryFile(mode='w')
579 bashrc = os.path.expanduser('~/.bashrc')
580 if os.path.exists(bashrc):
581 with open(bashrc, 'r') as src:
582 shutil.copyfileobj(src, tmprc)
583 tmprc.write('\nexport PS1="[%s] $PS1"' % gst_version)
585 if is_bash_completion_available(options):
586 bash_completions_files = []
587 for p in BASH_COMPLETION_PATHS:
588 if os.path.exists(p):
589 bash_completions_files += os.listdir(path=p)
590 bc_rc = BC_RC.format(bash_completions=' '.join(bash_completions_files), bash_completions_paths=' '.join(BASH_COMPLETION_PATHS))
593 args.append("--rcfile")
594 args.append(tmprc.name)
595 elif args[0].endswith('fish'):
596 # Ignore SIGINT while using fish as the shell to make it behave
597 # like other shells such as bash and zsh.
598 # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
599 signal.signal(signal.SIGINT, lambda x, y: True)
601 args.append('--init-command')
602 prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
604 echo -n '[{}] '(original_fish_prompt)
605 end'''.format(gst_version)
606 args.append(prompt_cmd)
607 elif args[0].endswith('zsh'):
608 tmpdir = tempfile.TemporaryDirectory()
609 # Let the GC remove the tmp file
610 tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w')
611 zshrc = os.path.expanduser('~/.zshrc')
612 if os.path.exists(zshrc):
613 with open(zshrc, 'r') as src:
614 shutil.copyfileobj(src, tmprc)
615 tmprc.write('\nexport PROMPT="[{}] $PROMPT"'.format(gst_version))
617 env['ZDOTDIR'] = tmpdir.name
619 if options.only_environment:
620 for name, value in env.items():
621 print('{}={}'.format(name, shlex.quote(value)))
622 print('export {}'.format(name))
624 if os.environ.get("CI_PROJECT_NAME"):
625 print("Ignoring SIGINT when running on the CI,"
626 " as we get spurious sigint in there for some reason.")
627 signal.signal(signal.SIGINT, signal.SIG_IGN)
628 exit(subprocess.call(args, close_fds=False, env=env))
630 except subprocess.CalledProcessError as e: