X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;f=gst-env.py;h=fb266adad34cc7b95b2e16fcee7153b2e5dfa80e;hb=27b0bfbe07526b2d603409c2877a23a957a555bd;hp=2124c9c578a50f999e8bdfe3a0adfa1a9ed20b63;hpb=245baadbce97279d7d8902769305c0fc10d5c2a6;p=platform%2Fupstream%2Fgstreamer.git diff --git a/gst-env.py b/gst-env.py index 2124c9c..fb266ad 100755 --- a/gst-env.py +++ b/gst-env.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 import argparse -import contextlib +import glob import json import os import platform import re -import site +import shlex import shutil import subprocess -import sys import tempfile import pathlib import signal +from functools import lru_cache +from pathlib import PurePath, Path +from sys import exit -from distutils.sysconfig import get_python_lib -from distutils.util import strtobool +from typing import Any from scripts.common import get_meson from scripts.common import git @@ -24,10 +25,12 @@ from scripts.common import get_wine_shortpath SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)) PREFIX_DIR = os.path.join(SCRIPTDIR, 'prefix') -# Use '_build' as the builddir instead of 'build' +# Look for the following build dirs: `build` `_build` `builddir` DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'build') if not os.path.exists(DEFAULT_BUILDDIR): DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, '_build') +if not os.path.exists(DEFAULT_BUILDDIR): + DEFAULT_BUILDDIR = os.path.join(SCRIPTDIR, 'builddir') TYPELIB_REG = re.compile(r'.*\.typelib$') SHAREDLIB_REG = re.compile(r'\.so|\.dylib|\.dll') @@ -37,6 +40,28 @@ SHAREDLIB_REG = re.compile(r'\.so|\.dylib|\.dll') GSTPLUGIN_FILEPATH_REG_TEMPLATE = r'.*/{libdir}/gstreamer-1.0/[^/]+$' GSTPLUGIN_FILEPATH_REG = None +BC_RC = ''' +BASH_COMPLETION_SCRIPTS="{bash_completions}" +BASH_COMPLETION_PATHS="{bash_completions_paths}" +for p in $BASH_COMPLETION_PATHS; do +for f in $BASH_COMPLETION_SCRIPTS; do + [ -f "$p/$f" ] && . "$p/$f" +done +done +''' +BASH_COMPLETION_PATHS = [SCRIPTDIR + '/subprojects/gstreamer/data/bash-completion/completions'] +BASH_COMPLETION_PATHS += [SCRIPTDIR + '/subprojects/gst-devtools/validate/data/bash-completion/completions'] + + +def str_to_bool(value: Any) -> bool: + """Return whether the provided string (or any value really) represents true. Otherwise false. + Just like plugin server stringToBoolean. + """ + if not value: + return False + return str(value).lower() in ("y", "yes", "t", "true", "on", "1") + + def listify(o): if isinstance(o, str): return [o] @@ -44,6 +69,7 @@ def listify(o): return o raise AssertionError('Object {!r} must be a string or a list'.format(o)) + def stringify(o): if isinstance(o, str): return o @@ -53,7 +79,10 @@ def stringify(o): raise AssertionError('Did not expect object {!r} to have more than one element'.format(o)) raise AssertionError('Object {!r} must be a string or a list'.format(o)) + def prepend_env_var(env, var, value, sysroot): + if var is None: + return if value.startswith(sysroot): value = value[len(sysroot):] # Try not to exceed maximum length limits for env vars on Windows @@ -67,28 +96,76 @@ def prepend_env_var(env, var, value, sysroot): env[var] = val + env_val env[var] = env[var].replace(os.pathsep + os.pathsep, os.pathsep).strip(os.pathsep) + +def get_target_install_filename(target, filename): + ''' + Checks whether this file is one of the files installed by the target + ''' + basename = os.path.basename(filename) + for install_filename in listify(target['install_filename']): + if install_filename.endswith(basename): + return install_filename + return None + + +def get_pkgconfig_variable_from_pcfile(pcfile, varname): + variables = {} + substre = re.compile('\$\{[^${}]+\}') + with pcfile.open('r', encoding='utf-8') as f: + for line in f: + if '=' not in line: + continue + key, value = line[:-1].split('=', 1) + subst = {} + for each in substre.findall(value): + substkey = each[2:-1] + subst[each] = variables.get(substkey, '') + for k, v in subst.items(): + value = value.replace(k, v) + variables[key] = value + return variables.get(varname, '') + + +@lru_cache() +def get_pkgconfig_variable(builddir, pcname, varname): + ''' + Parsing isn't perfect, but it's good enough. + ''' + pcfile = Path(builddir) / 'meson-private' / (pcname + '.pc') + if pcfile.is_file(): + return get_pkgconfig_variable_from_pcfile(pcfile, varname) + return subprocess.check_output(['pkg-config', pcname, '--variable=' + varname], + universal_newlines=True, encoding='utf-8') + + +def is_gio_module(target, filename, builddir): + if target['type'] != 'shared module': + return False + install_filename = get_target_install_filename(target, filename) + if not install_filename: + return False + giomoduledir = PurePath(get_pkgconfig_variable(builddir, 'gio-2.0', 'giomoduledir')) + fpath = PurePath(install_filename) + if fpath.parent != giomoduledir: + return False + return True + + def is_library_target_and_not_plugin(target, filename): ''' Don't add plugins to PATH/LD_LIBRARY_PATH because: 1. We don't need to 2. It causes us to exceed the PATH length limit on Windows and Wine ''' - if not target['type'].startswith('shared'): - return False - if not target['installed']: + if target['type'] != 'shared library': return False # Check if this output of that target is a shared library if not SHAREDLIB_REG.search(filename): return False # Check if it's installed to the gstreamer plugin location - for install_filename in listify(target['install_filename']): - if install_filename.endswith(os.path.basename(filename)): - break - else: - # None of the installed files in the target correspond to the built - # filename, so skip + install_filename = get_target_install_filename(target, filename) + if not install_filename: return False - global GSTPLUGIN_FILEPATH_REG if GSTPLUGIN_FILEPATH_REG is None: GSTPLUGIN_FILEPATH_REG = re.compile(GSTPLUGIN_FILEPATH_REG_TEMPLATE) @@ -97,6 +174,19 @@ def is_library_target_and_not_plugin(target, filename): return True +def is_binary_target_and_in_path(target, filename, bindir): + if target['type'] != 'executable': + return False + # Check if this file installed by this target is installed to bindir + install_filename = get_target_install_filename(target, filename) + if not install_filename: + return False + fpath = PurePath(install_filename) + if fpath.parent != bindir: + return False + return True + + def get_wine_subprocess_env(options, env): with open(os.path.join(options.builddir, 'meson-info', 'intro-buildoptions.json')) as f: buildoptions = json.load(f) @@ -116,23 +206,79 @@ def get_wine_subprocess_env(options, env): return env +def setup_gdb(options): + python_paths = set() + + if not shutil.which('gdb'): + return python_paths + + bdir = pathlib.Path(options.builddir).resolve() + for libpath, gdb_path in [ + (os.path.join("subprojects", "gstreamer", "gst"), + os.path.join("subprojects", "gstreamer", "libs", "gst", "helpers")), + (os.path.join("subprojects", "glib", "gobject"), None), + (os.path.join("subprojects", "glib", "glib"), None)]: + + if not gdb_path: + gdb_path = libpath + + autoload_path = (pathlib.Path(bdir) / 'gdb-auto-load').joinpath(*bdir.parts[1:]) / libpath + autoload_path.mkdir(parents=True, exist_ok=True) + for gdb_helper in glob.glob(str(bdir / gdb_path / "*-gdb.py")): + python_paths.add(str(bdir / gdb_path)) + python_paths.add(os.path.join(options.srcdir, gdb_path)) + try: + if os.name == 'nt': + shutil.copy(gdb_helper, str(autoload_path / os.path.basename(gdb_helper))) + else: + os.symlink(gdb_helper, str(autoload_path / os.path.basename(gdb_helper))) + except (FileExistsError, shutil.SameFileError): + pass + + gdbinit_line = 'add-auto-load-scripts-directory {}\n'.format(bdir / 'gdb-auto-load') + try: + with open(os.path.join(options.srcdir, '.gdbinit'), 'r') as f: + if gdbinit_line in f.readlines(): + return python_paths + except FileNotFoundError: + pass + + with open(os.path.join(options.srcdir, '.gdbinit'), 'a') as f: + f.write(gdbinit_line) + + return python_paths + + +def is_bash_completion_available(options): + return os.path.exists(os.path.join(options.builddir, 'subprojects/gstreamer/data/bash-completion/helpers/gst')) + + def get_subprocess_env(options, gst_version): env = os.environ.copy() env["CURRENT_GST"] = os.path.normpath(SCRIPTDIR) env["GST_VERSION"] = gst_version - env["GST_VALIDATE_SCENARIOS_PATH"] = os.path.normpath( - "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR) + prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath( + "%s/subprojects/gst-devtools/validate/data/scenarios" % SCRIPTDIR), + options.sysroot) env["GST_VALIDATE_PLUGIN_PATH"] = os.path.normpath( "%s/subprojects/gst-devtools/validate/plugins" % options.builddir) - env["GST_VALIDATE_APPS_DIR"] = os.path.normpath( - "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR) - env["GST_ENV"] = 'gst-' + gst_version + prepend_env_var(env, "GST_VALIDATE_APPS_DIR", os.path.normpath( + "%s/subprojects/gst-editing-services/tests/validate" % SCRIPTDIR), + options.sysroot) + env["GST_ENV"] = gst_version env["GST_REGISTRY"] = os.path.normpath(options.builddir + "/registry.dat") prepend_env_var(env, "PATH", os.path.normpath( "%s/subprojects/gst-devtools/validate/tools" % options.builddir), options.sysroot) + prepend_env_var(env, "GST_VALIDATE_SCENARIOS_PATH", os.path.normpath( + "%s/subprojects/gst-examples/webrtc/check/validate/scenarios" % + SCRIPTDIR), options.sysroot) + prepend_env_var(env, "GST_VALIDATE_APPS_DIR", os.path.normpath( + "%s/subprojects/gst-examples/webrtc/check/validate/apps" % + SCRIPTDIR), options.sysroot) + if options.wine: return get_wine_subprocess_env(options, env) @@ -148,7 +294,8 @@ def get_subprocess_env(options, gst_version): if os.name == 'nt': lib_path_envvar = 'PATH' elif platform.system() == 'Darwin': - lib_path_envvar = 'DYLD_LIBRARY_PATH' + # RPATH is sufficient on macOS, and DYLD_LIBRARY_PATH can cause issues with dynamic linker path priority + lib_path_envvar = None else: lib_path_envvar = 'LD_LIBRARY_PATH' @@ -172,13 +319,17 @@ def get_subprocess_env(options, gst_version): options.sysroot) # gst-indent - prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'gstreamer', 'tools'), + prepend_env_var(env, "PATH", os.path.join(SCRIPTDIR, 'scripts'), options.sysroot) # tools: gst-launch-1.0, gst-inspect-1.0 prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects', 'gstreamer', 'tools'), options.sysroot) + # plugin scanner and generator + prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects', + 'gstreamer', 'docs'), + options.sysroot) prepend_env_var(env, "PATH", os.path.join(options.builddir, 'subprojects', 'gst-plugins-base', 'tools'), options.sysroot) @@ -206,13 +357,19 @@ def get_subprocess_env(options, gst_version): build_options_s = subprocess.check_output(meson + ['introspect', options.builddir, '--buildoptions']) build_options = json.loads(build_options_s.decode()) libdir, = [o['value'] for o in build_options if o['name'] == 'libdir'] - libdir = libdir.replace('\\', '/') + libdir = PurePath(libdir) + prefix, = [o['value'] for o in build_options if o['name'] == 'prefix'] + bindir, = [o['value'] for o in build_options if o['name'] == 'bindir'] + prefix = PurePath(prefix) + bindir = prefix / bindir global GSTPLUGIN_FILEPATH_REG_TEMPLATE - GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir) + GSTPLUGIN_FILEPATH_REG_TEMPLATE = GSTPLUGIN_FILEPATH_REG_TEMPLATE.format(libdir=libdir.as_posix()) for target in targets: filenames = listify(target['filename']) + if not target['installed']: + continue for filename in filenames: root = os.path.dirname(filename) if srcdir_path / "subprojects/gst-devtools/validate/plugins" in (srcdir_path / root).parents: @@ -227,30 +384,40 @@ def get_subprocess_env(options, gst_version): prepend_env_var(env, lib_path_envvar, os.path.join(options.builddir, root), options.sysroot) - elif target['type'] == 'executable' and target['installed']: + elif is_binary_target_and_in_path(target, filename, bindir): paths.add(os.path.join(options.builddir, root)) + elif is_gio_module(target, filename, options.builddir): + prepend_env_var(env, 'GIO_EXTRA_MODULES', + os.path.join(options.builddir, root), + options.sysroot) - with open(os.path.join(options.builddir, 'GstPluginsPath.json')) as f: - for plugin_path in json.load(f): - prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path, - options.sysroot) + # Search for the Plugin paths file either in the build directory root + # or check if gstreamer is a subproject of another project + for sub_directories in [[], ['subprojects', 'gstreamer']]: + plugin_paths = os.path.join(options.builddir, *sub_directories, 'GstPluginsPath.json') + if os.path.exists(plugin_paths): + with open(plugin_paths) as f: + for plugin_path in json.load(f): + prepend_env_var(env, 'GST_PLUGIN_PATH', plugin_path, + options.sysroot) + break - for p in paths: + # Sort to iterate in a consistent order (`set`s and `hash`es are randomized) + for p in sorted(paths): prepend_env_var(env, 'PATH', p, options.sysroot) if os.name != 'nt': - for p in mono_paths: + for p in sorted(mono_paths): prepend_env_var(env, "MONO_PATH", p, options.sysroot) presets = set() encoding_targets = set() - pkg_dirs = set() - python_dirs = set(["%s/subprojects/gstreamer/libs/gst/helpers/" % options.srcdir]) + python_dirs = setup_gdb(options) + overrides_dirs = set() if '--installed' in subprocess.check_output(meson + ['introspect', '-h']).decode(): installed_s = subprocess.check_output(meson + ['introspect', options.builddir, '--installed']) for path, installpath in json.loads(installed_s.decode()).items(): installpath_parts = pathlib.Path(installpath).parts - path_parts = pathlib.Path(path).parts # We want to add all python modules to the PYTHONPATH # in a manner consistent with the way they would be imported: @@ -266,65 +433,77 @@ def get_subprocess_env(options, gst_version): if 'site-packages' in installpath_parts: install_subpath = os.path.join(*installpath_parts[installpath_parts.index('site-packages') + 1:]) if path.endswith(install_subpath): - python_dirs.add(path[:len (install_subpath) * -1]) + if os.path.commonprefix(["gi/overrides", install_subpath]): + overrides_dirs.add(os.path.dirname(path)) + else: + python_dirs.add(path[:len(install_subpath) * -1]) if path.endswith('.prs'): presets.add(os.path.dirname(path)) elif path.endswith('.gep'): encoding_targets.add( os.path.abspath(os.path.join(os.path.dirname(path), '..'))) - elif path.endswith('.pc'): - # Is there a -uninstalled pc file for this file? - uninstalled = "{0}-uninstalled.pc".format(path[:-3]) - if os.path.exists(uninstalled): - pkg_dirs.add(os.path.dirname(path)) if path.endswith('gstomx.conf'): prepend_env_var(env, 'GST_OMX_CONFIG_DIR', os.path.dirname(path), options.sysroot) - for p in presets: + for p in sorted(presets): prepend_env_var(env, 'GST_PRESET_PATH', p, options.sysroot) - for t in encoding_targets: + for t in sorted(encoding_targets): prepend_env_var(env, 'GST_ENCODING_TARGET_PATH', t, options.sysroot) - for pkg_dir in pkg_dirs: - prepend_env_var(env, "PKG_CONFIG_PATH", pkg_dir, options.sysroot) - prepend_env_var(env, "PKG_CONFIG_PATH", os.path.join(options.builddir, - 'subprojects', - 'gst-plugins-good', - 'pkgconfig'), - options.sysroot) + # Check if meson has generated -uninstalled pkgconfig files + meson_uninstalled = pathlib.Path(options.builddir) / 'meson-uninstalled' + if meson_uninstalled.is_dir(): + prepend_env_var(env, 'PKG_CONFIG_PATH', str(meson_uninstalled), options.sysroot) - for python_dir in python_dirs: + for python_dir in sorted(python_dirs): prepend_env_var(env, 'PYTHONPATH', python_dir, options.sysroot) + for python_dir in sorted(overrides_dirs): + prepend_env_var(env, '_GI_OVERRIDES_PATH', python_dir, options.sysroot) + mesonpath = os.path.join(SCRIPTDIR, "meson") if os.path.join(mesonpath): # Add meson/ into PYTHONPATH if we are using a local meson prepend_env_var(env, 'PYTHONPATH', mesonpath, options.sysroot) + # Ensure that gst-python/gi is used first + prepend_env_var(env, "PYTHONPATH", os.path.join(SCRIPTDIR, 'subprojects', 'gst-python'), + options.sysroot) + # For devhelp books - if not 'XDG_DATA_DIRS' in env or not env['XDG_DATA_DIRS']: + if 'XDG_DATA_DIRS' not in env or not env['XDG_DATA_DIRS']: # Preserve default paths when empty prepend_env_var(env, 'XDG_DATA_DIRS', '/usr/local/share/:/usr/share/', '') - prepend_env_var (env, 'XDG_DATA_DIRS', os.path.join(options.builddir, - 'subprojects', - 'gst-docs', - 'GStreamer-doc'), - options.sysroot) + prepend_env_var(env, 'XDG_DATA_DIRS', os.path.join(options.builddir, + 'subprojects', + 'gst-docs', + 'GStreamer-doc'), + options.sysroot) + + if 'XDG_CONFIG_DIRS' not in env or not env['XDG_CONFIG_DIRS']: + # Preserve default paths when empty + prepend_env_var(env, 'XDG_CONFIG_DIRS', '/etc/local/xdg:/etc/xdg', '') + + prepend_env_var(env, "XDG_CONFIG_DIRS", os.path.join(PREFIX_DIR, 'etc', 'xdg'), + options.sysroot) return env + def get_windows_shell(): - command = ['powershell.exe' ,'-noprofile', '-executionpolicy', 'bypass', '-file', 'cmd_or_ps.ps1'] + command = ['powershell.exe', '-noprofile', '-executionpolicy', 'bypass', '-file', + os.path.join(SCRIPTDIR, 'data', 'misc', 'cmd_or_ps.ps1')] result = subprocess.check_output(command) return result.decode().strip() + if __name__ == "__main__": - parser = argparse.ArgumentParser(prog="gstreamer-uninstalled") + parser = argparse.ArgumentParser(prog="gst-env") parser.add_argument("--builddir", default=DEFAULT_BUILDDIR, @@ -340,7 +519,11 @@ if __name__ == "__main__": help="Build a wine env based on specified wine command") parser.add_argument("--winepath", default='', - help="Exra path to set to WINEPATH.") + help="Extra path to set to WINEPATH.") + parser.add_argument("--only-environment", + action='store_true', + default=False, + help="Do not start a shell, only print required environment.") options, args = parser.parse_known_args() if not os.path.exists(options.builddir): @@ -355,37 +538,56 @@ if __name__ == "__main__": exit(1) # The following incantation will retrieve the current branch name. - gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD", - repository_path=options.srcdir).strip('\n') + try: + gst_version = git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD", + repository_path=options.srcdir).strip('\n') + except subprocess.CalledProcessError: + gst_version = "unknown" if options.wine: gst_version += '-' + os.path.basename(options.wine) - if not args: - if os.name == 'nt': - shell = get_windows_shell() - if shell == 'powershell.exe': - args = ['powershell.exe'] - args += ['-NoLogo', '-NoExit'] - prompt = 'function global:prompt { "[gst-' + gst_version + '"+"] PS " + $PWD + "> "}' - args += ['-Command', prompt] + env = get_subprocess_env(options, gst_version) + if os.name == 'nt': + shell = get_windows_shell() + if shell in ['powershell.exe', 'pwsh.exe']: + new_args = [shell, '-NoLogo'] + if not args: + prompt = 'function global:prompt { "[' + gst_version + '"+"] PS " + $PWD + "> "}' + new_args += ['-NoExit', '-Command', prompt] else: - args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")] - args += ['/k', 'prompt [gst-{}] $P$G'.format(gst_version)] + new_args += ['-NonInteractive', '-Command'] + args + args = new_args else: + new_args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")] + if not args: + new_args += ['/k', 'prompt [{}] $P$G'.format(gst_version)] + else: + new_args += ['/c', 'start', '/b', '/wait'] + args + args = new_args + if not args: + if os.name != 'nt': args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))] - if args[0].endswith('bash') and not strtobool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")): + if args[0].endswith('bash') and not str_to_bool(os.environ.get("GST_BUILD_DISABLE_PS1_OVERRIDE", r"FALSE")): + # Let the GC remove the tmp file tmprc = tempfile.NamedTemporaryFile(mode='w') bashrc = os.path.expanduser('~/.bashrc') if os.path.exists(bashrc): with open(bashrc, 'r') as src: shutil.copyfileobj(src, tmprc) - tmprc.write('\nexport PS1="[gst-%s] $PS1"' % gst_version) + tmprc.write('\nexport PS1="[%s] $PS1"' % gst_version) tmprc.flush() - # Let the GC remove the tmp file + if is_bash_completion_available(options): + bash_completions_files = [] + for p in BASH_COMPLETION_PATHS: + if os.path.exists(p): + bash_completions_files += os.listdir(path=p) + bc_rc = BC_RC.format(bash_completions=' '.join(bash_completions_files), bash_completions_paths=' '.join(BASH_COMPLETION_PATHS)) + tmprc.write(bc_rc) + tmprc.flush() args.append("--rcfile") args.append(tmprc.name) - if args[0].endswith('fish'): + elif args[0].endswith('fish'): # Ignore SIGINT while using fish as the shell to make it behave # like other shells such as bash and zsh. # See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18 @@ -394,11 +596,31 @@ if __name__ == "__main__": args.append('--init-command') prompt_cmd = '''functions --copy fish_prompt original_fish_prompt function fish_prompt - echo -n '[gst-{}] '(original_fish_prompt) + echo -n '[{}] '(original_fish_prompt) end'''.format(gst_version) args.append(prompt_cmd) + elif args[0].endswith('zsh'): + tmpdir = tempfile.TemporaryDirectory() + # Let the GC remove the tmp file + tmprc = open(os.path.join(tmpdir.name, '.zshrc'), 'w') + zshrc = os.path.expanduser('~/.zshrc') + if os.path.exists(zshrc): + with open(zshrc, 'r') as src: + shutil.copyfileobj(src, tmprc) + tmprc.write('\nexport PROMPT="[{}] $PROMPT"'.format(gst_version)) + tmprc.flush() + env['ZDOTDIR'] = tmpdir.name try: - exit(subprocess.call(args, close_fds=False, - env=get_subprocess_env(options, gst_version))) + if options.only_environment: + for name, value in env.items(): + print('{}={}'.format(name, shlex.quote(value))) + print('export {}'.format(name)) + else: + if os.environ.get("CI_PROJECT_NAME"): + print("Ignoring SIGINT when running on the CI," + " as we get spurious sigint in there for some reason.") + signal.signal(signal.SIGINT, signal.SIG_IGN) + exit(subprocess.call(args, close_fds=False, env=env)) + except subprocess.CalledProcessError as e: exit(e.returncode)