18 from functools import lru_cache
19 from pathlib import PurePath, Path
21 from typing import Any
23 from scripts.common import get_meson
24 from scripts.common import git
25 from scripts.common import win32_get_short_path_name
26 from scripts.common import get_wine_shortpath
28 SCRIPTDIR = os.path.dirname(os.path.realpath(__file__))
29 PREFIX_DIR = os.path.join(SCRIPTDIR, 'prefix')
30 # Look for the following build dirs: `build` `_build` `builddir`
31 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'build')
32 if not os.path.exists(DEFAULT_BUILDDIR):
33 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, '_build')
34 if not os.path.exists(DEFAULT_BUILDDIR):
35 DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'builddir')
37 TYPELIB_REG = re.compile(r'.*\.typelib$')
38 SHAREDLIB_REG = re.compile(r'\.so|\.dylib|\.dll')
40 # libdir is expanded from option of the same name listed in the `meson
41 # introspect --buildoptions` output.
42 GSTPLUGIN_FILEPATH_REG_TEMPLATE = r'.*/{libdir}/gstreamer-1.0/[^/]+$'
43 GSTPLUGIN_FILEPATH_REG = None
46 BASH_COMPLETION_SCRIPTS="{bash_completions}"
47 BASH_COMPLETION_PATHS="{bash_completions_paths}"
48 for p in $BASH_COMPLETION_PATHS; do
49 for f in $BASH_COMPLETION_SCRIPTS; do
50 [ -f "$p/$f" ] && . "$p/$f"
54 BASH_COMPLETION_PATHS = [SCRIPTDIR + '/subprojects/gstreamer/data/bash-completion/completions']
55 BASH_COMPLETION_PATHS += [SCRIPTDIR + '/subprojects/gst-devtools/validate/data/bash-completion/completions']
58 def str_to_bool(value: Any) -> bool:
59 """Return whether the provided string (or any value really) represents true. Otherwise false.
60 Just like plugin server stringToBoolean.
64 return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
68 if isinstance(o, str):
70 if isinstance(o, list):
72 raise AssertionError('Object {!r} must be a string or a list'.format(o))
75 if isinstance(o, str):
77 if isinstance(o, list):
80 raise AssertionError('Did not expect object {!r} to have more than one element'.format(o))
81 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)
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
109 def get_pkgconfig_variable_from_pcfile(pcfile, varname):
111 substre = re.compile('\$\{[^${}]+\}')
112 with pcfile.open('r', encoding='utf-8') as f:
116 key, value = line[:-1].split('=', 1)
118 for each in substre.findall(value):
119 substkey = each[2:-1]
120 subst[each] = variables.get(substkey, '')
121 for k, v in subst.items():
122 value = value.replace(k, v)
123 variables[key] = value
124 return variables.get(varname, '')
127 def get_pkgconfig_variable(builddir, pcname, varname):
129 Parsing isn't perfect, but it's good enough.
131 pcfile = Path(builddir) / 'meson-private' / (pcname + '.pc')
133 return get_pkgconfig_variable_from_pcfile(pcfile, varname)
134 return subprocess.check_output(['pkg-config', pcname, '--variable=' + varname],
135 universal_newlines=True, encoding='utf-8')
138 def is_gio_module(target, filename, builddir):
139 if target['type'] != 'shared module':
141 install_filename = get_target_install_filename(target, filename)
142 if not install_filename:
144 giomoduledir = PurePath(get_pkgconfig_variable(builddir, 'gio-2.0', 'giomoduledir'))
145 fpath = PurePath(install_filename)
146 if fpath.parent != giomoduledir:
150 def is_library_target_and_not_plugin(target, filename):
152 Don't add plugins to PATH/LD_LIBRARY_PATH because:
154 2. It causes us to exceed the PATH length limit on Windows and Wine
156 if target['type'] != 'shared library':
158 # Check if this output of that target is a shared library
159 if not SHAREDLIB_REG.search(filename):
161 # Check if it's installed to the gstreamer plugin location
162 install_filename = get_target_install_filename(target, filename)
163 if not install_filename:
165 global GSTPLUGIN_FILEPATH_REG
166 if GSTPLUGIN_FILEPATH_REG is None:
167 GSTPLUGIN_FILEPATH_REG = re.compile(GSTPLUGIN_FILEPATH_REG_TEMPLATE)
168 if GSTPLUGIN_FILEPATH_REG.search(install_filename.replace('\\', '/')):
172 def is_binary_target_and_in_path(target, filename, bindir):
173 if target['type'] != 'executable':
175 # Check if this file installed by this target is installed to bindir
176 install_filename = get_target_install_filename(target, filename)
177 if not install_filename:
179 fpath = PurePath(install_filename)
180 if fpath.parent != bindir:
185 def get_wine_subprocess_env(options, env):
186 with open(os.path.join(options.builddir, 'meson-info', 'intro-buildoptions.json')) as f:
187 buildoptions = json.load(f)
189 prefix, = [o for o in buildoptions if o['name'] == 'prefix']
190 path = os.path.normpath(os.path.join(prefix['value'], 'bin'))
191 prepend_env_var(env, "PATH", path, options.sysroot)
192 wine_path = get_wine_shortpath(
193 options.wine.split(' '),
194 [path] + env.get('WINEPATH', '').split(';')
197 wine_path += ';' + options.winepath
198 env['WINEPATH'] = wine_path
199 env['WINEDEBUG'] = 'fixme-all'
203 def setup_gdb(options):
206 if not shutil.which('gdb'):
209 bdir = pathlib.Path(options.builddir).resolve()
210 for libpath, gdb_path in [
211 (os.path.join("subprojects", "gstreamer", "gst"),
212 os.path.join("subprojects", "gstreamer", "libs", "gst", "helpers")),
213 (os.path.join("subprojects", "glib", "gobject"), None),
214 (os.path.join("subprojects", "glib", "glib"), None)]:
219 autoload_path = (pathlib.Path(bdir) / 'gdb-auto-load').joinpath(*bdir.parts[1:]) / libpath
220 autoload_path.mkdir(parents=True, exist_ok=True)
221 for gdb_helper in glob.glob(str(bdir / gdb_path / "*-gdb.py")):
222 python_paths.add(str(bdir / gdb_path))
223 python_paths.add(os.path.join(options.srcdir, gdb_path))
226 shutil.copy(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
228 os.symlink(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
229 except (FileExistsError, shutil.SameFileError):
232 gdbinit_line = 'add-auto-load-scripts-directory {}\n'.format(bdir / 'gdb-auto-load')
234 with open(os.path.join(options.srcdir, '.gdbinit'), 'r') as f:
235 if gdbinit_line in f.readlines():
237 except FileNotFoundError:
240 with open(os.path.join(options.srcdir, '.gdbinit'), 'a') as f:
241 f.write(gdbinit_line)
245 def is_bash_completion_available (options):
246 return os.path.exists(os.path.join(options.builddir, 'subprojects/gstreamer/data/bash-completion/helpers/gst'))
248 def get_subprocess_env(options, gst_version):
249 env = os.environ.copy()
251 env["CURRENT_GST"] = os.path.normpath(SCRIPTDIR)
252 env["GST_VERSION"] = gst_version
253 prepend_env_var (env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath(
254 "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR),
256 env["GST_VALIDATE_PLUGIN_PATH"] = os.path.normpath(
257 "%s/subprojects/gst-devtools/validate/plugins" % options.builddir)
258 prepend_env_var (env, "GST_VALIDATE_APPS_DIR", os.path.normpath(
259 "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR),
261 env["GST_ENV"] = 'gst-' + gst_version
262 env["GST_REGISTRY"] = os.path.normpath(options.builddir + "/registry.dat")
263 prepend_env_var(env, "PATH", os.path.normpath(
264 "%s/subprojects/gst-devtools/validate/tools" % options.builddir),
267 prepend_env_var (env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath(
268 "%s/subprojects/gst-examples/webrtc/check/validate/scenarios" %
269 SCRIPTDIR), options.sysroot)
270 prepend_env_var (env, "GST_VALIDATE_APPS_DIR", os.path.normpath(
271 "%s/subprojects/gst-examples/webrtc/check/validate/apps" %
272 SCRIPTDIR), options.sysroot)
275 return get_wine_subprocess_env(options, env)
277 prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'meson'),
280 env["GST_PLUGIN_SYSTEM_PATH"] = ""
281 env["GST_PLUGIN_SCANNER"] = os.path.normpath(
282 "%s/subprojects/gstreamer/libs/gst/helpers/gst-plugin-scanner" % options.builddir)
283 env["GST_PTP_HELPER"] = os.path.normpath(
284 "%s/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper" % options.builddir)
287 lib_path_envvar = 'PATH'
288 elif platform.system() == 'Darwin':
289 # RPATH is sufficient on macOS, and DYLD_LIBRARY_PATH can cause issues with dynamic linker path priority
290 lib_path_envvar = None
292 lib_path_envvar = 'LD_LIBRARY_PATH'
294 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(SCRIPTDIR, 'subprojects',
295 'gst-python', 'plugin'),
297 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(PREFIX_DIR, 'lib',
300 prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(options.builddir, 'subprojects',
303 prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH",
304 os.path.join(PREFIX_DIR, 'share', 'gstreamer-1.0',
305 'validate', 'scenarios'),
307 prepend_env_var(env, "GI_TYPELIB_PATH", os.path.join(PREFIX_DIR, 'lib',
308 'lib', 'girepository-1.0'),
310 prepend_env_var(env, "PKG_CONFIG_PATH", os.path.join(PREFIX_DIR, 'lib', 'pkgconfig'),
314 prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'scripts'),
317 # tools: gst-launch-1.0, gst-inspect-1.0
318 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
319 'gstreamer', 'tools'),
321 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
322 'gst-plugins-base', 'tools'),
325 # Library and binary search paths
326 prepend_env_var(env, "PATH", os.path.join(PREFIX_DIR, 'bin'),
328 if lib_path_envvar != 'PATH':
329 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib'),
331 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib64'),
333 elif 'QMAKE' in os.environ:
334 # There's no RPATH on Windows, so we need to set PATH for the qt5 DLLs
335 prepend_env_var(env, 'PATH', os.path.dirname(os.environ['QMAKE']),
339 targets_s = subprocess.check_output(meson + ['introspect', options.builddir, '--targets'])
340 targets = json.loads(targets_s.decode())
343 srcdir_path = pathlib.Path(options.srcdir)
345 build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions'])
346 build_options = json.loads(build_options_s.decode())
347 libdir, = [o['value'] for o in build_options if o['name'] == 'libdir']
348 libdir = PurePath(libdir)
349 prefix, = [o['value'] for o in build_options if o['name'] == 'prefix']
350 bindir, = [o['value'] for o in build_options if o['name'] == 'bindir']
351 prefix = PurePath(prefix)
352 bindir = prefix / bindir
354 global GSTPLUGIN_FILEPATH_REG_TEMPLATE
355 GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix())
357 for target in targets:
358 filenames = listify(target['filename'])
359 if not target['installed']:
361 for filename in filenames:
362 root = os.path.dirname(filename)
363 if srcdir_path / "subprojects/gst-devtools/validate/plugins" in (srcdir_path / root).parents:
365 if filename.endswith('.dll'):
366 mono_paths.add(os.path.join(options.builddir, root))
367 if TYPELIB_REG.search(filename):
368 prepend_env_var(env, "GI_TYPELIB_PATH",
369 os.path.join(options.builddir, root),
371 elif is_library_target_and_not_plugin(target, filename):
372 prepend_env_var(env, lib_path_envvar,
373 os.path.join(options.builddir, root),
375 elif is_binary_target_and_in_path(target, filename, bindir):
376 paths.add(os.path.join(options.builddir, root))
377 elif is_gio_module(target, filename, options.builddir):
378 prepend_env_var(env, 'GIO_EXTRA_MODULES',
379 os.path.join(options.builddir, root),
382 with open(os.path.join(options.gstbuilddir, 'GstPluginsPath.json')) as f:
383 for plugin_path in json.load(f):
384 prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path,
387 # Sort to iterate in a consistent order (`set`s and `hash`es are randomized)
388 for p in sorted(paths):
389 prepend_env_var(env, 'PATH', p, options.sysroot)
392 for p in sorted(mono_paths):
393 prepend_env_var(env, "MONO_PATH", p, options.sysroot)
396 encoding_targets = set()
397 python_dirs = setup_gdb(options)
398 overrides_dirs = set()
399 if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode():
400 installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed'])
401 for path, installpath in json.loads(installed_s.decode()).items():
402 installpath_parts = pathlib.Path(installpath).parts
403 path_parts = pathlib.Path(path).parts
405 # We want to add all python modules to the PYTHONPATH
406 # in a manner consistent with the way they would be imported:
407 # For example if the source path /home/meh/foo/bar.py
408 # is to be installed in /usr/lib/python/site-packages/foo/bar.py,
409 # we want to add /home/meh to the PYTHONPATH.
410 # This will only work for projects where the paths to be installed
411 # mirror the installed directory layout, for example if the path
412 # is /home/meh/baz/bar.py and the install path is
413 # /usr/lib/site-packages/foo/bar.py , we will not add anything
414 # to PYTHONPATH, but the current approach works with pygobject
415 # and gst-python at least.
416 if 'site-packages' in installpath_parts:
417 install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:])
418 if path.endswith(install_subpath):
419 if os.path.commonprefix(["gi/overrides", install_subpath]):
420 overrides_dirs.add(os.path.dirname(path))
422 python_dirs.add(path[:len (install_subpath) * -1])
424 if path.endswith('.prs'):
425 presets.add(os.path.dirname(path))
426 elif path.endswith('.gep'):
427 encoding_targets.add(
428 os.path.abspath(os.path.join(os.path.dirname(path), '..')))
430 if path.endswith('gstomx.conf'):
431 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
434 for p in sorted(presets):
435 prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
437 for t in sorted(encoding_targets):
438 prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
440 # Check if meson has generated -uninstalled pkgconfig files
441 meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
442 if meson_uninstalled.is_dir():
443 prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
445 for python_dir in sorted(python_dirs):
446 prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
448 for python_dir in sorted(overrides_dirs):
449 prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot)
451 mesonpath = os.path.join(SCRIPTDIR, "meson")
452 if os.path.join(mesonpath):
453 # Add meson/ into PYTHONPATH if we are using a local meson
454 prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
456 # Ensure that gst-python/gi is used first
457 prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'),
461 if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
462 # Preserve default paths when empty
463 prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
465 prepend_env_var (env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
471 if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
472 # Preserve default paths when empty
473 prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
475 prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
480 def get_windows_shell():
481 command = ['powershell.exe' ,'-noprofile', '-executionpolicy', 'bypass', '-file',
482 os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')]
483 result = subprocess.check_output(command)
484 return result.decode().strip()
486 if __name__ == "__main__":
487 parser = argparse.ArgumentParser(prog="gst-env")
489 parser.add_argument("--builddir",
490 default=DEFAULT_BUILDDIR,
491 help="The meson build directory")
492 parser.add_argument("--gstbuilddir",
494 help="The meson GStreamer build directory (defaults to builddir)")
495 parser.add_argument("--srcdir",
497 help="The top level source directory")
498 parser.add_argument("--sysroot",
500 help="The sysroot path used during cross-compilation")
501 parser.add_argument("--wine",
503 help="Build a wine env based on specified wine command")
504 parser.add_argument("--winepath",
506 help="Extra path to set to WINEPATH.")
507 parser.add_argument("--only-environment",
510 help="Do not start a shell, only print required environment.")
511 options, args = parser.parse_known_args()
513 if not os.path.exists(options.builddir):
514 print("GStreamer not built in %s\n\nBuild it and try again" %
518 if options.gstbuilddir and not os.path.exists(options.gstbuilddir):
519 print("GStreamer is not built in %s\n\nBuild it and try again" %
522 elif not options.gstbuilddir:
523 options.gstbuilddir = options.builddir
525 options.builddir = os.path.abspath(options.builddir)
526 options.gstbuilddir = os.path.abspath(options.gstbuilddir)
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: