gst-env.py: Sort path sets before using prepend_env_var
[platform/upstream/gstreamer.git] / gst-env.py
1 #!/usr/bin/env python3
2
3 import argparse
4 import contextlib
5 import glob
6 import json
7 import os
8 import platform
9 import re
10 import site
11 import shlex
12 import shutil
13 import subprocess
14 import sys
15 import tempfile
16 import pathlib
17 import signal
18 from pathlib import PurePath
19
20 from distutils.sysconfig import get_python_lib
21 from distutils.util import strtobool
22
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
27
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')
36
37 TYPELIB_REG = re.compile(r'.*\.typelib$')
38 SHAREDLIB_REG = re.compile(r'\.so|\.dylib|\.dll')
39
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
44
45 def listify(o):
46     if isinstance(o, str):
47         return [o]
48     if isinstance(o, list):
49         return o
50     raise AssertionError('Object {!r} must be a string or a list'.format(o))
51
52 def stringify(o):
53     if isinstance(o, str):
54         return o
55     if isinstance(o, list):
56         if len(o) == 1:
57             return o[0]
58         raise AssertionError('Did not expect object {!r} to have more than one element'.format(o))
59     raise AssertionError('Object {!r} must be a string or a list'.format(o))
60
61 def prepend_env_var(env, var, value, sysroot):
62     if value.startswith(sysroot):
63         value = value[len(sysroot):]
64     # Try not to exceed maximum length limits for env vars on Windows
65     if os.name == 'nt':
66         value = win32_get_short_path_name(value)
67     env_val = env.get(var, '')
68     val = os.pathsep + value + os.pathsep
69     # Don't add the same value twice
70     if val in env_val or env_val.startswith(value + os.pathsep):
71         return
72     env[var] = val + env_val
73     env[var] = env[var].replace(os.pathsep + os.pathsep, os.pathsep).strip(os.pathsep)
74
75 def is_library_target_and_not_plugin(target, filename):
76     '''
77     Don't add plugins to PATH/LD_LIBRARY_PATH because:
78     1. We don't need to
79     2. It causes us to exceed the PATH length limit on Windows and Wine
80     '''
81     if not target['type'].startswith('shared'):
82         return False
83     if not target['installed']:
84         return False
85     # Check if this output of that target is a shared library
86     if not SHAREDLIB_REG.search(filename):
87         return False
88     # Check if it's installed to the gstreamer plugin location
89     for install_filename in listify(target['install_filename']):
90         if install_filename.endswith(os.path.basename(filename)):
91             break
92     else:
93         # None of the installed files in the target correspond to the built
94         # filename, so skip
95         return False
96
97     global GSTPLUGIN_FILEPATH_REG
98     if GSTPLUGIN_FILEPATH_REG is None:
99         GSTPLUGIN_FILEPATH_REG = re.compile(GSTPLUGIN_FILEPATH_REG_TEMPLATE)
100     if GSTPLUGIN_FILEPATH_REG.search(install_filename.replace('\\', '/')):
101         return False
102     return True
103
104 def is_binary_target_and_in_path(target, filename, bindir):
105     if target['type'] != 'executable':
106         return False
107     if not target['installed']:
108         return False
109     # Check if this file installed by this target is installed to bindir
110     for install_filename in listify(target['install_filename']):
111         if install_filename.endswith(os.path.basename(filename)):
112             break
113     else:
114         # None of the installed files in the target correspond to the built
115         # filename, so skip
116         return False
117     fpath = PurePath(install_filename)
118     if fpath.parent != bindir:
119         return False
120     return True
121
122
123 def get_wine_subprocess_env(options, env):
124     with open(os.path.join(options.builddir, 'meson-info', 'intro-buildoptions.json')) as f:
125         buildoptions = json.load(f)
126
127     prefix, = [o for o in buildoptions if o['name'] == 'prefix']
128     path = os.path.normpath(os.path.join(prefix['value'], 'bin'))
129     prepend_env_var(env, "PATH", path, options.sysroot)
130     wine_path = get_wine_shortpath(
131         options.wine.split(' '),
132         [path] + env.get('WINEPATH', '').split(';')
133     )
134     if options.winepath:
135         wine_path += ';' + options.winepath
136     env['WINEPATH'] = wine_path
137     env['WINEDEBUG'] = 'fixme-all'
138
139     return env
140
141 def setup_gdb(options):
142     python_paths = set()
143
144     if not shutil.which('gdb'):
145         return python_paths
146
147     bdir = pathlib.Path(options.builddir).resolve()
148     for libpath, gdb_path in [
149             (os.path.join("subprojects", "gstreamer", "gst"),
150              os.path.join("subprojects", "gstreamer", "libs", "gst", "helpers")),
151             (os.path.join("subprojects", "glib", "gobject"), None),
152             (os.path.join("subprojects", "glib", "glib"), None)]:
153
154         if not gdb_path:
155             gdb_path = libpath
156
157         autoload_path = (pathlib.Path(bdir) / 'gdb-auto-load').joinpath(*bdir.parts[1:]) / libpath
158         autoload_path.mkdir(parents=True, exist_ok=True)
159         for gdb_helper in glob.glob(str(bdir / gdb_path / "*-gdb.py")):
160             python_paths.add(str(bdir / gdb_path))
161             python_paths.add(os.path.join(options.srcdir, gdb_path))
162             try:
163                 if os.name == 'nt':
164                     shutil.copy(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
165                 else:
166                     os.symlink(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
167             except (FileExistsError, shutil.SameFileError):
168                 pass
169
170     gdbinit_line = 'add-auto-load-scripts-directory {}\n'.format(bdir / 'gdb-auto-load')
171     try:
172         with open(os.path.join(options.srcdir, '.gdbinit'), 'r') as f:
173             if gdbinit_line in f.readlines():
174                 return python_paths
175     except FileNotFoundError:
176         pass
177
178     with open(os.path.join(options.srcdir, '.gdbinit'), 'a') as f:
179         f.write(gdbinit_line)
180
181     return python_paths
182
183
184 def get_subprocess_env(options, gst_version):
185     env = os.environ.copy()
186
187     env["CURRENT_GST"] = os.path.normpath(SCRIPTDIR)
188     env["GST_VERSION"] = gst_version
189     env["GST_VALIDATE_SCENARIOS_PATH"] = os.path.normpath(
190         "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR)
191     env["GST_VALIDATE_PLUGIN_PATH"] = os.path.normpath(
192         "%s/subprojects/gst-devtools/validate/plugins" % options.builddir)
193     env["GST_VALIDATE_APPS_DIR"] = os.path.normpath(
194         "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR)
195     env["GST_ENV"] = 'gst-' + gst_version
196     env["GST_REGISTRY"] = os.path.normpath(options.builddir + "/registry.dat")
197     prepend_env_var(env, "PATH", os.path.normpath(
198         "%s/subprojects/gst-devtools/validate/tools" % options.builddir),
199         options.sysroot)
200
201     if options.wine:
202         return get_wine_subprocess_env(options, env)
203
204     prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'meson'),
205         options.sysroot)
206
207     env["GST_PLUGIN_SYSTEM_PATH"] = ""
208     env["GST_PLUGIN_SCANNER"] = os.path.normpath(
209         "%s/subprojects/gstreamer/libs/gst/helpers/gst-plugin-scanner" % options.builddir)
210     env["GST_PTP_HELPER"] = os.path.normpath(
211         "%s/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper" % options.builddir)
212
213     if os.name == 'nt':
214         lib_path_envvar = 'PATH'
215     elif platform.system() == 'Darwin':
216         lib_path_envvar = 'DYLD_LIBRARY_PATH'
217     else:
218         lib_path_envvar = 'LD_LIBRARY_PATH'
219
220     prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(SCRIPTDIR, 'subprojects',
221                                                         'gst-python', 'plugin'),
222                     options.sysroot)
223     prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(PREFIX_DIR, 'lib',
224                                                         'gstreamer-1.0'),
225                     options.sysroot)
226     prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(options.builddir, 'subprojects',
227                                                          'libnice', 'gst'),
228                     options.sysroot)
229     prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH",
230                     os.path.join(PREFIX_DIR, 'share', 'gstreamer-1.0',
231                                  'validate', 'scenarios'),
232                     options.sysroot)
233     prepend_env_var(env, "GI_TYPELIB_PATH", os.path.join(PREFIX_DIR, 'lib',
234                                                          'lib', 'girepository-1.0'),
235                     options.sysroot)
236     prepend_env_var(env, "PKG_CONFIG_PATH", os.path.join(PREFIX_DIR, 'lib', 'pkgconfig'),
237                     options.sysroot)
238
239     # gst-indent
240     prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'gstreamer', 'tools'),
241                     options.sysroot)
242
243     # tools: gst-launch-1.0, gst-inspect-1.0
244     prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
245                                               'gstreamer', 'tools'),
246                     options.sysroot)
247     prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
248                                               'gst-plugins-base', 'tools'),
249                     options.sysroot)
250
251     # Library and binary search paths
252     prepend_env_var(env, "PATH", os.path.join(PREFIX_DIR, 'bin'),
253                     options.sysroot)
254     if lib_path_envvar != 'PATH':
255         prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib'),
256                         options.sysroot)
257         prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib64'),
258                         options.sysroot)
259     elif 'QMAKE' in os.environ:
260         # There's no RPATH on Windows, so we need to set PATH for the qt5 DLLs
261         prepend_env_var(env, 'PATH', os.path.dirname(os.environ['QMAKE']),
262                         options.sysroot)
263
264     meson = get_meson()
265     targets_s = subprocess.check_output(meson + ['introspect', options.builddir, '--targets'])
266     targets = json.loads(targets_s.decode())
267     paths = set()
268     mono_paths = set()
269     srcdir_path = pathlib.Path(options.srcdir)
270
271     build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions'])
272     build_options = json.loads(build_options_s.decode())
273     libdir, = [o['value'] for o in build_options if o['name'] == 'libdir']
274     libdir = PurePath(libdir)
275     prefix, = [o['value'] for o in build_options if o['name'] == 'prefix']
276     bindir, = [o['value'] for o in build_options if o['name'] == 'bindir']
277     prefix = PurePath(prefix)
278     bindir = prefix / bindir
279
280     global GSTPLUGIN_FILEPATH_REG_TEMPLATE
281     GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix())
282
283     for target in targets:
284         filenames = listify(target['filename'])
285         for filename in filenames:
286             root = os.path.dirname(filename)
287             if srcdir_path / "subprojects/gst-devtools/validate/plugins" in (srcdir_path / root).parents:
288                 continue
289             if filename.endswith('.dll'):
290                 mono_paths.add(os.path.join(options.builddir, root))
291             if TYPELIB_REG.search(filename):
292                 prepend_env_var(env, "GI_TYPELIB_PATH",
293                                 os.path.join(options.builddir, root),
294                                 options.sysroot)
295             elif is_library_target_and_not_plugin(target, filename):
296                 prepend_env_var(env, lib_path_envvar,
297                                 os.path.join(options.builddir, root),
298                                 options.sysroot)
299             elif is_binary_target_and_in_path(target, filename, bindir):
300                 paths.add(os.path.join(options.builddir, root))
301
302     with open(os.path.join(options.builddir, 'GstPluginsPath.json')) as f:
303         for plugin_path in json.load(f):
304             prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path,
305                             options.sysroot)
306
307     # Sort to iterate in a consistent order (`set`s and `hash`es are randomized)
308     for p in sorted(paths):
309         prepend_env_var(env, 'PATH', p, options.sysroot)
310
311     if os.name != 'nt':
312         for p in sorted(mono_paths):
313             prepend_env_var(env, "MONO_PATH", p, options.sysroot)
314
315     presets = set()
316     encoding_targets = set()
317     pkg_dirs = set()
318     python_dirs = setup_gdb(options)
319     if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode():
320         installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed'])
321         for path, installpath in json.loads(installed_s.decode()).items():
322             installpath_parts = pathlib.Path(installpath).parts
323             path_parts = pathlib.Path(path).parts
324
325             # We want to add all python modules to the PYTHONPATH
326             # in a manner consistent with the way they would be imported:
327             # For example if the source path /home/meh/foo/bar.py
328             # is to be installed in /usr/lib/python/site-packages/foo/bar.py,
329             # we want to add /home/meh to the PYTHONPATH.
330             # This will only work for projects where the paths to be installed
331             # mirror the installed directory layout, for example if the path
332             # is /home/meh/baz/bar.py and the install path is
333             # /usr/lib/site-packages/foo/bar.py , we will not add anything
334             # to PYTHONPATH, but the current approach works with pygobject
335             # and gst-python at least.
336             if 'site-packages' in installpath_parts:
337                 install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:])
338                 if path.endswith(install_subpath):
339                     python_dirs.add(path[:len (install_subpath) * -1])
340
341             if path.endswith('.prs'):
342                 presets.add(os.path.dirname(path))
343             elif path.endswith('.gep'):
344                 encoding_targets.add(
345                     os.path.abspath(os.path.join(os.path.dirname(path), '..')))
346             elif path.endswith('.pc'):
347                 # Is there a -uninstalled pc file for this file?
348                 uninstalled = "{0}-uninstalled.pc".format(path[:-3])
349                 if os.path.exists(uninstalled):
350                     pkg_dirs.add(os.path.dirname(path))
351
352             if path.endswith('gstomx.conf'):
353                 prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path),
354                                 options.sysroot)
355
356         for p in sorted(presets):
357             prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot)
358
359         for t in sorted(encoding_targets):
360             prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot)
361
362         for pkg_dir in sorted(pkg_dirs):
363             prepend_env_var(env, "PKG_CONFIG_PATH", pkg_dir, options.sysroot)
364
365     # Check if meson has generated -uninstalled pkgconfig files
366     meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled'
367     if meson_uninstalled.is_dir():
368         prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot)
369
370     for python_dir in sorted(python_dirs):
371         prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot)
372
373     mesonpath = os.path.join(SCRIPTDIR, "meson")
374     if os.path.join(mesonpath):
375         # Add meson/ into PYTHONPATH if we are using a local meson
376         prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot)
377
378     # For devhelp books
379     if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']:
380         # Preserve default paths when empty
381         prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '')
382
383     prepend_env_var (env, 'XDG_DATA_DIRS', os.path.join(options.builddir,
384                                                         'subprojects',
385                                                         'gst-docs',
386                                                         'GStreamer-doc'),
387                      options.sysroot)
388
389     if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']:
390         # Preserve default paths when empty
391         prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '')
392
393     prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'),
394                     options.sysroot)
395
396     return env
397
398 def get_windows_shell():
399     command = ['powershell.exe' ,'-noprofile', '-executionpolicy', 'bypass', '-file', 'cmd_or_ps.ps1']
400     result = subprocess.check_output(command)
401     return result.decode().strip()
402
403 if __name__ == "__main__":
404     parser = argparse.ArgumentParser(prog="gst-env")
405
406     parser.add_argument("--builddir",
407                         default=DEFAULT_BUILDDIR,
408                         help="The meson build directory")
409     parser.add_argument("--srcdir",
410                         default=SCRIPTDIR,
411                         help="The top level source directory")
412     parser.add_argument("--sysroot",
413                         default='',
414                         help="The sysroot path used during cross-compilation")
415     parser.add_argument("--wine",
416                         default='',
417                         help="Build a wine env based on specified wine command")
418     parser.add_argument("--winepath",
419                         default='',
420                         help="Extra path to set to WINEPATH.")
421     parser.add_argument("--only-environment",
422                         action='store_true',
423                         default=False,
424                         help="Do not start a shell, only print required environment.")
425     options, args = parser.parse_known_args()
426
427     if not os.path.exists(options.builddir):
428         print("GStreamer not built in %s\n\nBuild it and try again" %
429               options.builddir)
430         exit(1)
431     options.builddir = os.path.abspath(options.builddir)
432
433     if not os.path.exists(options.srcdir):
434         print("The specified source dir does not exist" %
435               options.srcdir)
436         exit(1)
437
438     # The following incantation will retrieve the current branch name.
439     try:
440       gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
441                         repository_path=options.srcdir).strip('\n')
442     except subprocess.CalledProcessError:
443       gst_version = "unknown"
444
445     if options.wine:
446         gst_version += '-' + os.path.basename(options.wine)
447
448     env = get_subprocess_env(options, gst_version)
449     if not args:
450         if os.name == 'nt':
451             shell = get_windows_shell()
452             if shell == 'powershell.exe':
453                 args = ['powershell.exe']
454                 args += ['-NoLogo', '-NoExit']
455                 prompt = 'function global:prompt {  "[gst-' + gst_version + '"+"] PS " + $PWD + "> "}'
456                 args += ['-Command', prompt]
457             else:
458                 args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
459                 args += ['/k', 'prompt [gst-{}] $P$G'.format(gst_version)]
460         else:
461             args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
462         if args[0].endswith('bash') and not strtobool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
463             # Let the GC remove the tmp file
464             tmprc = tempfile.NamedTemporaryFile(mode='w')
465             bashrc = os.path.expanduser('~/.bashrc')
466             if os.path.exists(bashrc):
467                 with open(bashrc, 'r') as src:
468                     shutil.copyfileobj(src, tmprc)
469             tmprc.write('\nexport PS1="[gst-%s] $PS1"' % gst_version)
470             tmprc.flush()
471             args.append("--rcfile")
472             args.append(tmprc.name)
473         elif args[0].endswith('fish'):
474             # Ignore SIGINT while using fish as the shell to make it behave
475             # like other shells such as bash and zsh.
476             # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
477             signal.signal(signal.SIGINT, lambda x, y: True)
478             # Set the prompt
479             args.append('--init-command')
480             prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
481             function fish_prompt
482                 echo -n '[gst-{}] '(original_fish_prompt)
483             end'''.format(gst_version)
484             args.append(prompt_cmd)
485         elif args[0].endswith('zsh'):
486             tmpdir = tempfile.TemporaryDirectory()
487             # Let the GC remove the tmp file
488             tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w')
489             zshrc = os.path.expanduser('~/.zshrc')
490             if os.path.exists(zshrc):
491                 with open(zshrc, 'r') as src:
492                     shutil.copyfileobj(src, tmprc)
493             tmprc.write('\nexport PROMPT="[gst-{}] $PROMPT"'.format(gst_version))
494             tmprc.flush()
495             env['ZDOTDIR'] = tmpdir.name
496     try:
497         if options.only_environment:
498             for name, value in env.items():
499                 print('{}={}'.format(name, shlex.quote(value)))
500                 print('export {}'.format(name))
501         else:
502             exit(subprocess.call(args, close_fds=False, env=env))
503
504     except subprocess.CalledProcessError as e:
505         exit(e.returncode)