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