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