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 # plugin scanner and generator
322 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
323 'gstreamer', 'docs'),
325 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
326 'gst-plugins-base', 'tools'),
329 # Library and binary search paths
330 prepend_env_var(env, "PATH", os.path.join(PREFIX_DIR, 'bin'),
332 if lib_path_envvar != 'PATH':
333 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib'),
335 prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib64'),
337 elif 'QMAKE' in os.environ:
338 # There's no RPATH on Windows, so we need to set PATH for the qt5 DLLs
339 prepend_env_var(env, 'PATH', os.path.dirname(os.environ['QMAKE']),
343 targets_s = subprocess.check_output(meson + ['introspect', options.builddir, '--targets'])
344 targets = json.loads(targets_s.decode())
347 srcdir_path = pathlib.Path(options.srcdir)
349 build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions'])
350 build_options = json.loads(build_options_s.decode())
351 libdir, = [o['value'] for o in build_options if o['name'] == 'libdir']
352 libdir = PurePath(libdir)
353 prefix, = [o['value'] for o in build_options if o['name'] == 'prefix']
354 bindir, = [o['value'] for o in build_options if o['name'] == 'bindir']
355 prefix = PurePath(prefix)
356 bindir = prefix / bindir
358 global GSTPLUGIN_FILEPATH_REG_TEMPLATE
359 GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix())
361 for target in targets:
362 filenames = listify(target['filename'])
363 if not target['installed']:
365 for filename in filenames:
366 root = os.path.dirname(filename)
367 if srcdir_path / "subprojects/gst-devtools/validate/plugins" in (srcdir_path / root).parents:
369 if filename.endswith('.dll'):
370 mono_paths.add(os.path.join(options.builddir, root))
371 if TYPELIB_REG.search(filename):
372 prepend_env_var(env, "GI_TYPELIB_PATH",
373 os.path.join(options.builddir, root),
375 elif is_library_target_and_not_plugin(target, filename):
376 prepend_env_var(env, lib_path_envvar,
377 os.path.join(options.builddir, root),
379 elif is_binary_target_and_in_path(target, filename, bindir):
380 paths.add(os.path.join(options.builddir, root))
381 elif is_gio_module(target, filename, options.builddir):
382 prepend_env_var(env, 'GIO_EXTRA_MODULES',
383 os.path.join(options.builddir, root),
386 with open(os.path.join(options.gstbuilddir, 'GstPluginsPath.json')) as f:
387 for plugin_path in json.load(f):
388 prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path,
391 # Sort to iterate in a consistent order (`set`s and `hash`es are randomized)
392 for p in sorted(paths):
393 prepend_env_var(env, 'PATH', p, options.sysroot)
396 for p in sorted(mono_paths):
397 prepend_env_var(env, "MONO_PATH", p, options.sysroot)
400 encoding_targets = set()
401 python_dirs = setup_gdb(options)
402 overrides_dirs = set()
403 if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode():
404 installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed'])
405 for path, installpath in json.loads(installed_s.decode()).items():
406 installpath_parts = pathlib.Path(installpath).parts
407 path_parts = pathlib.Path(path).parts
409 # We want to add all python modules to the PYTHONPATH
410 # in a manner consistent with the way they would be imported:
411 # For example if the source path /home/meh/foo/bar.py
412 # is to be installed in /usr/lib/python/site-packages/foo/bar.py,
413 # we want to add /home/meh to the PYTHONPATH.
414 # This will only work for projects where the paths to be installed
415 # mirror the installed directory layout, for example if the path
416 # is /home/meh/baz/bar.py and the install path is
417 # /usr/lib/site-packages/foo/bar.py , we will not add anything
418 # to PYTHONPATH, but the current approach works with pygobject
419 # and gst-python at least.
420 if 'site-packages' in installpath_parts:
421 install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:])
422 if path.endswith(install_subpath):
423 if os.path.commonprefix(["gi/overrides", install_subpath]):
424 overrides_dirs.add(os.path.dirname(path))
426 python_dirs.add(path[:len (install_subpath) * -1])
428 if path.endswith('.prs'):
429 presets.add(os.path.dirname(path))
430 elif path.endswith('.gep'):
431 encoding_targets.add(
432 os.path.abspath(os.path.join(os.path.dirname(path), '..')))
434 if path.endswith('gstomx.conf'):
435 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
438 for p in sorted(presets):
439 prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
441 for t in sorted(encoding_targets):
442 prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
444 # Check if meson has generated -uninstalled pkgconfig files
445 meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
446 if meson_uninstalled.is_dir():
447 prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
449 for python_dir in sorted(python_dirs):
450 prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
452 for python_dir in sorted(overrides_dirs):
453 prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot)
455 mesonpath = os.path.join(SCRIPTDIR, "meson")
456 if os.path.join(mesonpath):
457 # Add meson/ into PYTHONPATH if we are using a local meson
458 prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
460 # Ensure that gst-python/gi is used first
461 prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'),
465 if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
466 # Preserve default paths when empty
467 prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
469 prepend_env_var (env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
475 if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
476 # Preserve default paths when empty
477 prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
479 prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
484 def get_windows_shell():
485 command = ['powershell.exe' ,'-noprofile', '-executionpolicy', 'bypass', '-file',
486 os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')]
487 result = subprocess.check_output(command)
488 return result.decode().strip()
490 if __name__ == "__main__":
491 parser = argparse.ArgumentParser(prog="gst-env")
493 parser.add_argument("--builddir",
494 default=DEFAULT_BUILDDIR,
495 help="The meson build directory")
496 parser.add_argument("--gstbuilddir",
498 help="The meson GStreamer build directory (defaults to builddir)")
499 parser.add_argument("--srcdir",
501 help="The top level source directory")
502 parser.add_argument("--sysroot",
504 help="The sysroot path used during cross-compilation")
505 parser.add_argument("--wine",
507 help="Build a wine env based on specified wine command")
508 parser.add_argument("--winepath",
510 help="Extra path to set to WINEPATH.")
511 parser.add_argument("--only-environment",
514 help="Do not start a shell, only print required environment.")
515 options, args = parser.parse_known_args()
517 if not os.path.exists(options.builddir):
518 print("GStreamer not built in %s\n\nBuild it and try again" %
522 if options.gstbuilddir and not os.path.exists(options.gstbuilddir):
523 print("GStreamer is not built in %s\n\nBuild it and try again" %
526 elif not options.gstbuilddir:
527 options.gstbuilddir = options.builddir
529 options.builddir = os.path.abspath(options.builddir)
530 options.gstbuilddir = os.path.abspath(options.gstbuilddir)
532 if not os.path.exists(options.srcdir):
533 print("The specified source dir does not exist" %
537 # The following incantation will retrieve the current branch name.
539 gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
540 repository_path=options.srcdir).strip('\n')
541 except subprocess.CalledProcessError:
542 gst_version = "unknown"
545 gst_version += '-' + os.path.basename(options.wine)
547 env = get_subprocess_env(options, gst_version)
550 shell = get_windows_shell()
551 if shell == 'powershell.exe':
552 args = ['powershell.exe']
553 args += ['-NoLogo', '-NoExit']
554 prompt = 'function global:prompt { "[gst-' + gst_version + '"+"] PS " + $PWD + "> "}'
555 args += ['-Command', prompt]
557 args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
558 args += ['/k', 'prompt [gst-{}] $P$G'.format(gst_version)]
560 args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
561 if args[0].endswith('bash') and not str_to_bool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
562 # Let the GC remove the tmp file
563 tmprc = tempfile.NamedTemporaryFile(mode='w')
564 bashrc = os.path.expanduser('~/.bashrc')
565 if os.path.exists(bashrc):
566 with open(bashrc, 'r') as src:
567 shutil.copyfileobj(src, tmprc)
568 tmprc.write('\nexport PS1="[gst-%s] $PS1"' % gst_version)
570 if is_bash_completion_available(options):
571 bash_completions_files = []
572 for p in BASH_COMPLETION_PATHS:
573 if os.path.exists(p):
574 bash_completions_files += os.listdir(path=p)
575 bc_rc = BC_RC.format(bash_completions=' '.join(bash_completions_files), bash_completions_paths=' '.join(BASH_COMPLETION_PATHS))
578 args.append("--rcfile")
579 args.append(tmprc.name)
580 elif args[0].endswith('fish'):
581 # Ignore SIGINT while using fish as the shell to make it behave
582 # like other shells such as bash and zsh.
583 # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
584 signal.signal(signal.SIGINT, lambda x, y: True)
586 args.append('--init-command')
587 prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
589 echo -n '[gst-{}] '(original_fish_prompt)
590 end'''.format(gst_version)
591 args.append(prompt_cmd)
592 elif args[0].endswith('zsh'):
593 tmpdir = tempfile.TemporaryDirectory()
594 # Let the GC remove the tmp file
595 tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w')
596 zshrc = os.path.expanduser('~/.zshrc')
597 if os.path.exists(zshrc):
598 with open(zshrc, 'r') as src:
599 shutil.copyfileobj(src, tmprc)
600 tmprc.write('\nexport PROMPT="[gst-{}] $PROMPT"'.format(gst_version))
602 env['ZDOTDIR'] = tmpdir.name
604 if options.only_environment:
605 for name, value in env.items():
606 print('{}={}'.format(name, shlex.quote(value)))
607 print('export {}'.format(name))
609 if os.environ.get("CI_PROJECT_NAME"):
610 print("Ignoring SIGINT when running on the CI,"
611 " as we get spurious sigint in there for some reason.")
612 signal.signal(signal.SIGINT, signal.SIG_IGN)
613 exit(subprocess.call(args, close_fds=False, env=env))
615 except subprocess.CalledProcessError as e: