gst-env: Factor out some common code
[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 get_target_install_filename(target, filename):
76     '''
77     Checks whether this file is one of the files installed by the target
78     '''
79     basename = os.path.basename(filename)
80     for install_filename in listify(target['install_filename']):
81         if install_filename.endswith(basename):
82             return install_filename
83     return None
84
85
86 def is_library_target_and_not_plugin(target, filename):
87     '''
88     Don't add plugins to PATH/LD_LIBRARY_PATH because:
89     1. We don't need to
90     2. It causes us to exceed the PATH length limit on Windows and Wine
91     '''
92     if not target['type'].startswith('shared'):
93         return False
94     # Check if this output of that target is a shared library
95     if not SHAREDLIB_REG.search(filename):
96         return False
97     # Check if it's installed to the gstreamer plugin location
98     install_filename = get_target_install_filename(target, filename)
99     if not install_filename:
100         return False
101     global GSTPLUGIN_FILEPATH_REG
102     if GSTPLUGIN_FILEPATH_REG is None:
103         GSTPLUGIN_FILEPATH_REG = re.compile(GSTPLUGIN_FILEPATH_REG_TEMPLATE)
104     if GSTPLUGIN_FILEPATH_REG.search(install_filename.replace('\\', '/')):
105         return False
106     return True
107
108 def is_binary_target_and_in_path(target, filename, bindir):
109     if target['type'] != 'executable':
110         return False
111     # Check if this file installed by this target is installed to bindir
112     install_filename = get_target_install_filename(target, filename)
113     if not install_filename:
114         return False
115     fpath = PurePath(install_filename)
116     if fpath.parent != bindir:
117         return False
118     return True
119
120
121 def get_wine_subprocess_env(options, env):
122     with open(os.path.join(options.builddir, 'meson-info', 'intro-buildoptions.json')) as f:
123         buildoptions = json.load(f)
124
125     prefix, = [o for o in buildoptions if o['name'] == 'prefix']
126     path = os.path.normpath(os.path.join(prefix['value'], 'bin'))
127     prepend_env_var(env, "PATH", path, options.sysroot)
128     wine_path = get_wine_shortpath(
129         options.wine.split(' '),
130         [path] + env.get('WINEPATH', '').split(';')
131     )
132     if options.winepath:
133         wine_path += ';' + options.winepath
134     env['WINEPATH'] = wine_path
135     env['WINEDEBUG'] = 'fixme-all'
136
137     return env
138
139 def setup_gdb(options):
140     python_paths = set()
141
142     if not shutil.which('gdb'):
143         return python_paths
144
145     bdir = pathlib.Path(options.builddir).resolve()
146     for libpath, gdb_path in [
147             (os.path.join("subprojects", "gstreamer", "gst"),
148              os.path.join("subprojects", "gstreamer", "libs", "gst", "helpers")),
149             (os.path.join("subprojects", "glib", "gobject"), None),
150             (os.path.join("subprojects", "glib", "glib"), None)]:
151
152         if not gdb_path:
153             gdb_path = libpath
154
155         autoload_path = (pathlib.Path(bdir) / 'gdb-auto-load').joinpath(*bdir.parts[1:]) / libpath
156         autoload_path.mkdir(parents=True, exist_ok=True)
157         for gdb_helper in glob.glob(str(bdir / gdb_path / "*-gdb.py")):
158             python_paths.add(str(bdir / gdb_path))
159             python_paths.add(os.path.join(options.srcdir, gdb_path))
160             try:
161                 if os.name == 'nt':
162                     shutil.copy(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
163                 else:
164                     os.symlink(gdb_helper, str(autoload_path / os.path.basename(gdb_helper)))
165             except (FileExistsError, shutil.SameFileError):
166                 pass
167
168     gdbinit_line = 'add-auto-load-scripts-directory {}\n'.format(bdir / 'gdb-auto-load')
169     try:
170         with open(os.path.join(options.srcdir, '.gdbinit'), 'r') as f:
171             if gdbinit_line in f.readlines():
172                 return python_paths
173     except FileNotFoundError:
174         pass
175
176     with open(os.path.join(options.srcdir, '.gdbinit'), 'a') as f:
177         f.write(gdbinit_line)
178
179     return python_paths
180
181
182 def get_subprocess_env(options, gst_version):
183     env = os.environ.copy()
184
185     env["CURRENT_GST"] = os.path.normpath(SCRIPTDIR)
186     env["GST_VERSION"] = gst_version
187     env["GST_VALIDATE_SCENARIOS_PATH"] = os.path.normpath(
188         "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR)
189     env["GST_VALIDATE_PLUGIN_PATH"] = os.path.normpath(
190         "%s/subprojects/gst-devtools/validate/plugins" % options.builddir)
191     env["GST_VALIDATE_APPS_DIR"] = os.path.normpath(
192         "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR)
193     env["GST_ENV"] = 'gst-' + gst_version
194     env["GST_REGISTRY"] = os.path.normpath(options.builddir + "/registry.dat")
195     prepend_env_var(env, "PATH", os.path.normpath(
196         "%s/subprojects/gst-devtools/validate/tools" % options.builddir),
197         options.sysroot)
198
199     if options.wine:
200         return get_wine_subprocess_env(options, env)
201
202     prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'meson'),
203         options.sysroot)
204
205     env["GST_PLUGIN_SYSTEM_PATH"] = ""
206     env["GST_PLUGIN_SCANNER"] = os.path.normpath(
207         "%s/subprojects/gstreamer/libs/gst/helpers/gst-plugin-scanner" % options.builddir)
208     env["GST_PTP_HELPER"] = os.path.normpath(
209         "%s/subprojects/gstreamer/libs/gst/helpers/gst-ptp-helper" % options.builddir)
210
211     if os.name == 'nt':
212         lib_path_envvar = 'PATH'
213     elif platform.system() == 'Darwin':
214         lib_path_envvar = 'DYLD_LIBRARY_PATH'
215     else:
216         lib_path_envvar = 'LD_LIBRARY_PATH'
217
218     prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(SCRIPTDIR, 'subprojects',
219                                                         'gst-python', 'plugin'),
220                     options.sysroot)
221     prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(PREFIX_DIR, 'lib',
222                                                         'gstreamer-1.0'),
223                     options.sysroot)
224     prepend_env_var(env, "GST_PLUGIN_PATH", os.path.join(options.builddir, 'subprojects',
225                                                          'libnice', 'gst'),
226                     options.sysroot)
227     prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH",
228                     os.path.join(PREFIX_DIR, 'share', 'gstreamer-1.0',
229                                  'validate', 'scenarios'),
230                     options.sysroot)
231     prepend_env_var(env, "GI_TYPELIB_PATH", os.path.join(PREFIX_DIR, 'lib',
232                                                          'lib', 'girepository-1.0'),
233                     options.sysroot)
234     prepend_env_var(env, "PKG_CONFIG_PATH", os.path.join(PREFIX_DIR, 'lib', 'pkgconfig'),
235                     options.sysroot)
236
237     # gst-indent
238     prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'gstreamer', 'tools'),
239                     options.sysroot)
240
241     # tools: gst-launch-1.0, gst-inspect-1.0
242     prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
243                                               'gstreamer', 'tools'),
244                     options.sysroot)
245     prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects',
246                                               'gst-plugins-base', 'tools'),
247                     options.sysroot)
248
249     # Library and binary search paths
250     prepend_env_var(env, "PATH", os.path.join(PREFIX_DIR, 'bin'),
251                     options.sysroot)
252     if lib_path_envvar != 'PATH':
253         prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib'),
254                         options.sysroot)
255         prepend_env_var(env, lib_path_envvar, os.path.join(PREFIX_DIR, 'lib64'),
256                         options.sysroot)
257     elif 'QMAKE' in os.environ:
258         # There's no RPATH on Windows, so we need to set PATH for the qt5 DLLs
259         prepend_env_var(env, 'PATH', os.path.dirname(os.environ['QMAKE']),
260                         options.sysroot)
261
262     meson = get_meson()
263     targets_s = subprocess.check_output(meson + ['introspect', options.builddir, '--targets'])
264     targets = json.loads(targets_s.decode())
265     paths = set()
266     mono_paths = set()
267     srcdir_path = pathlib.Path(options.srcdir)
268
269     build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions'])
270     build_options = json.loads(build_options_s.decode())
271     libdir, = [o['value'] for o in build_options if o['name'] == 'libdir']
272     libdir = PurePath(libdir)
273     prefix, = [o['value'] for o in build_options if o['name'] == 'prefix']
274     bindir, = [o['value'] for o in build_options if o['name'] == 'bindir']
275     prefix = PurePath(prefix)
276     bindir = prefix / bindir
277
278     global GSTPLUGIN_FILEPATH_REG_TEMPLATE
279     GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix())
280
281     for target in targets:
282         filenames = listify(target['filename'])
283         if not target['installed']:
284             continue
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)