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.
433 if 'site-packages' in installpath_parts:
434 install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:])
435 if path.endswith(install_subpath):
436 if os.path.commonprefix(["gi/overrides", install_subpath]):
437 overrides_dirs.add(os.path.dirname(path))
439 python_dirs.add(path[:len(install_subpath) * -1])
441 if path.endswith('.prs'):
442 presets.add(os.path.dirname(path))
443 elif path.endswith('.gep'):
444 encoding_targets.add(
445 os.path.abspath(os.path.join(os.path.dirname(path), '..')))
447 if path.endswith('gstomx.conf'):
448 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
451 for p in sorted(presets):
452 prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
454 for t in sorted(encoding_targets):
455 prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
457 # Check if meson has generated -uninstalled pkgconfig files
458 meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
459 if meson_uninstalled.is_dir():
460 prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
462 for python_dir in sorted(python_dirs):
463 prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
465 for python_dir in sorted(overrides_dirs):
466 prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot)
468 mesonpath = os.path.join(SCRIPTDIR, "meson")
469 if os.path.join(mesonpath):
470 # Add meson/ into PYTHONPATH if we are using a local meson
471 prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
473 # Ensure that gst-python/gi is used first
474 prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'),
478 if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
479 # Preserve default paths when empty
480 prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
482 prepend_env_var(env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
488 if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
489 # Preserve default paths when empty
490 prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
492 prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
498 def get_windows_shell():
499 command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file',
500 os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')]
501 result = subprocess.check_output(command)
502 return result.decode().strip()
505 if __name__ == "__main__":
506 parser = argparse.ArgumentParser(prog="gst-env")
508 parser.add_argument("--builddir",
509 default=DEFAULT_BUILDDIR,
510 help="The meson build directory")
511 parser.add_argument("--srcdir",
513 help="The top level source directory")
514 parser.add_argument("--sysroot",
516 help="The sysroot path used during cross-compilation")
517 parser.add_argument("--wine",
519 help="Build a wine env based on specified wine command")
520 parser.add_argument("--winepath",
522 help="Extra path to set to WINEPATH.")
523 parser.add_argument("--only-environment",
526 help="Do not start a shell, only print required environment.")
527 options, args = parser.parse_known_args()
529 if not os.path.exists(options.builddir):
530 print("GStreamer not built in %s\n\nBuild it and try again" %
533 options.builddir = os.path.abspath(options.builddir)
535 if not os.path.exists(options.srcdir):
536 print("The specified source dir does not exist" %
540 # The following incantation will retrieve the current branch name.
542 gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
543 repository_path=options.srcdir).strip('\n')
544 except subprocess.CalledProcessError:
545 gst_version = "unknown"
548 gst_version += '-' + os.path.basename(options.wine)
550 env = get_subprocess_env(options, gst_version)
552 shell = get_windows_shell()
553 if shell in ['powershell.exe', 'pwsh.exe']:
554 new_args = [shell, '-NoLogo']
556 prompt = 'function global:prompt { "[' + gst_version + '"+"] PS " + $PWD + "> "}'
557 new_args += ['-NoExit', '-Command', prompt]
559 new_args += ['-NonInteractive', '-Command'] + args
562 new_args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
564 new_args += ['/k', 'prompt [{}] $P$G'.format(gst_version)]
566 new_args += ['/c', 'start', '/b', '/wait'] + args
570 args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
571 if args[0].endswith('bash') and not str_to_bool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
572 # Let the GC remove the tmp file
573 tmprc = tempfile.NamedTemporaryFile(mode='w')
574 bashrc = os.path.expanduser('~/.bashrc')
575 if os.path.exists(bashrc):
576 with open(bashrc, 'r') as src:
577 shutil.copyfileobj(src, tmprc)
578 tmprc.write('\nexport PS1="[%s] $PS1"' % gst_version)
580 if is_bash_completion_available(options):
581 bash_completions_files = []
582 for p in BASH_COMPLETION_PATHS:
583 if os.path.exists(p):
584 bash_completions_files += os.listdir(path=p)
585 bc_rc = BC_RC.format(bash_completions=' '.join(bash_completions_files), bash_completions_paths=' '.join(BASH_COMPLETION_PATHS))
588 args.append("--rcfile")
589 args.append(tmprc.name)
590 elif args[0].endswith('fish'):
591 # Ignore SIGINT while using fish as the shell to make it behave
592 # like other shells such as bash and zsh.
593 # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
594 signal.signal(signal.SIGINT, lambda x, y: True)
596 args.append('--init-command')
597 prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
599 echo -n '[{}] '(original_fish_prompt)
600 end'''.format(gst_version)
601 args.append(prompt_cmd)
602 elif args[0].endswith('zsh'):
603 tmpdir = tempfile.TemporaryDirectory()
604 # Let the GC remove the tmp file
605 tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w')
606 zshrc = os.path.expanduser('~/.zshrc')
607 if os.path.exists(zshrc):
608 with open(zshrc, 'r') as src:
609 shutil.copyfileobj(src, tmprc)
610 tmprc.write('\nexport PROMPT="[{}] $PROMPT"'.format(gst_version))
612 env['ZDOTDIR'] = tmpdir.name
614 if options.only_environment:
615 for name, value in env.items():
616 print('{}={}'.format(name, shlex.quote(value)))
617 print('export {}'.format(name))
619 if os.environ.get("CI_PROJECT_NAME"):
620 print("Ignoring SIGINT when running on the CI,"
621 " as we get spurious sigint in there for some reason.")
622 signal.signal(signal.SIGINT, signal.SIG_IGN)
623 exit(subprocess.call(args, close_fds=False, env=env))
625 except subprocess.CalledProcessError as e: