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