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