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