gst-env: Don't put helper binaries in PATH
[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 not 'XDG_DATA_DIRS' 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     return env
343
344 def get_windows_shell():
345     command = ['powershell.exe' ,'-noprofile', '-executionpolicy', 'bypass', '-file', 'cmd_or_ps.ps1']
346     result = subprocess.check_output(command)
347     return result.decode().strip()
348
349 if __name__ == "__main__":
350     parser = argparse.ArgumentParser(prog="gstreamer-uninstalled")
351
352     parser.add_argument("--builddir",
353                         default=DEFAULT_BUILDDIR,
354                         help="The meson build directory")
355     parser.add_argument("--srcdir",
356                         default=SCRIPTDIR,
357                         help="The top level source directory")
358     parser.add_argument("--sysroot",
359                         default='',
360                         help="The sysroot path used during cross-compilation")
361     parser.add_argument("--wine",
362                         default='',
363                         help="Build a wine env based on specified wine command")
364     parser.add_argument("--winepath",
365                         default='',
366                         help="Exra path to set to WINEPATH.")
367     options, args = parser.parse_known_args()
368
369     if not os.path.exists(options.builddir):
370         print("GStreamer not built in %s\n\nBuild it and try again" %
371               options.builddir)
372         exit(1)
373     options.builddir = os.path.abspath(options.builddir)
374
375     if not os.path.exists(options.srcdir):
376         print("The specified source dir does not exist" %
377               options.srcdir)
378         exit(1)
379
380     # The following incantation will retrieve the current branch name.
381     gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD",
382                       repository_path=options.srcdir).strip('\n')
383
384     if options.wine:
385         gst_version += '-' + os.path.basename(options.wine)
386
387     if not args:
388         if os.name == 'nt':
389             shell = get_windows_shell()
390             if shell == 'powershell.exe':
391                 args = ['powershell.exe']
392                 args += ['-NoLogo', '-NoExit']
393                 prompt = 'function global:prompt {  "[gst-' + gst_version + '"+"] PS " + $PWD + "> "}'
394                 args += ['-Command', prompt]
395             else:
396                 args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
397                 args += ['/k', 'prompt [gst-{}] $P$G'.format(gst_version)]
398         else:
399             args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
400         if args[0].endswith('bash') and not strtobool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")):
401             tmprc = tempfile.NamedTemporaryFile(mode='w')
402             bashrc = os.path.expanduser('~/.bashrc')
403             if os.path.exists(bashrc):
404                 with open(bashrc, 'r') as src:
405                     shutil.copyfileobj(src, tmprc)
406             tmprc.write('\nexport PS1="[gst-%s] $PS1"' % gst_version)
407             tmprc.flush()
408             # Let the GC remove the tmp file
409             args.append("--rcfile")
410             args.append(tmprc.name)
411         if args[0].endswith('fish'):
412             # Ignore SIGINT while using fish as the shell to make it behave
413             # like other shells such as bash and zsh.
414             # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
415             signal.signal(signal.SIGINT, lambda x, y: True)
416             # Set the prompt
417             args.append('--init-command')
418             prompt_cmd = '''functions --copy fish_prompt original_fish_prompt
419             function fish_prompt
420                 echo -n '[gst-{}] '(original_fish_prompt)
421             end'''.format(gst_version)
422             args.append(prompt_cmd)
423     try:
424         exit(subprocess.call(args, close_fds=False,
425                              env=get_subprocess_env(options, gst_version)))
426     except subprocess.CalledProcessError as e:
427         exit(e.returncode)