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