| # Copyright (C) 2018, 2020, 2021 Igalia S.L. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are met: |
| # |
| # 1. Redistributions of source code must retain the above copyright notice, this |
| # list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright notice, |
| # this list of conditions and the following disclaimer in the documentation |
| # and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| import logging |
| import os |
| import shutil |
| import subprocess |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class BinaryBundler: |
| VAR_MYDIR = 'MYDIR' |
| |
| def __init__(self, destination_dir, build_path=None): |
| self._destination_dir = destination_dir |
| self._has_patched_interpreter_relpath = False |
| self._build_path = build_path |
| self._should_use_sys_lib_directory = False |
| |
| def _is_system_dep(self, path): |
| if self._build_path is not None: |
| # We don't consider libwpe and libWPEBackend-fdo syslibs (even when those are not built directly). Neither dlopenwrap |
| if any(p in path.lower() for p in ['libwpe', 'dlopenwrap']): |
| return False |
| return not path.startswith(self._build_path) |
| return False |
| |
| def set_use_sys_lib_directory(self, should_use_sys_lib_directory): |
| self._should_use_sys_lib_directory = should_use_sys_lib_directory |
| |
| def copy_and_maybe_strip_patchelf(self, orig_file, type='bin', strip=True, patchelf_removerpath=True, patchelf_nodefaultlib=False, patchelf_setinterpreter_relativepath=None, object_final_destination_dir=None): |
| """ This does the following: |
| 1. Copies the binary/lib (object) |
| 2. Optionally runs patchelf with different parameters |
| 3. Optionally strips the binary |
| If the file is a symlink pointing another file then the symlink is preserved (copying the file pointed) |
| """ |
| |
| dir_suffix = 'lib' if type == 'interpreter' else type |
| if object_final_destination_dir is None: |
| object_final_destination_dir = self._destination_dir |
| # If set_use_sys_lib_directory() is enabled then system libraries are shipped in $ROOT/sys/lib to allow to redistribute them separately. |
| # However, the interpreter is always shipped in $ROOT/lib and all the binaries are always shipped in $ROOT/bin |
| # Even when they are system_deps because of patchelf_setinterpreter_relativepath |
| if self._should_use_sys_lib_directory and self._is_system_dep(orig_file) and type not in ['interpreter', 'bin']: |
| object_final_destination_dir = os.path.join(object_final_destination_dir, 'sys') |
| object_final_destination_dir = os.path.join(object_final_destination_dir, dir_suffix) |
| if not os.path.isdir(object_final_destination_dir): |
| os.makedirs(object_final_destination_dir) |
| |
| if not os.path.isfile(orig_file): |
| raise ValueError('Can not find file %s' % orig_file) |
| |
| _log.info('Add to bundle [%s]: %s' % (type, orig_file)) |
| |
| # Preserve symlinks and copy the targets |
| if os.path.islink(orig_file): |
| symlink_src_file = os.path.realpath(orig_file) |
| symlink_src_basename = os.path.basename(symlink_src_file) |
| symlink_dst_basename = os.path.basename(orig_file) |
| symlink_dst_fullpath = os.path.join(object_final_destination_dir, symlink_dst_basename) |
| if symlink_src_basename != symlink_dst_basename: |
| try: |
| # symlink_dst_fullpath -> symlink_src_basename |
| os.symlink(symlink_src_basename, symlink_dst_fullpath) |
| except FileExistsError: |
| previous_symlink_src_path = os.path.realpath(symlink_dst_fullpath) |
| previous_symlink_src_basename = os.path.basename(previous_symlink_src_path) |
| if previous_symlink_src_basename != symlink_src_basename: |
| raise RuntimeError('Not overwriting previous symlink %s pointing to %s with a symlink pointing to %s' % (symlink_dst_fullpath, previous_symlink_src_basename, symlink_src_basename)) |
| return self.copy_and_maybe_strip_patchelf(symlink_src_file, type, strip, patchelf_removerpath, patchelf_nodefaultlib, patchelf_setinterpreter_relativepath, object_final_destination_dir) |
| |
| try: |
| shutil.copy(orig_file, object_final_destination_dir) |
| except shutil.SameFileError: |
| # May reasonably happen if the caller tries to bundle the files 'in place'. |
| pass |
| |
| if type == 'interpreter': |
| return # no strip/patchelf over it |
| |
| # Running 'strip' after 'patchelf' may corrupt binaries, so run first 'strip'. |
| # See: https://github.com/NixOS/patchelf/issues/371 |
| if strip: |
| if not shutil.which('strip'): |
| _log.warning('Unable to find the strip command in the system. Please install it.') |
| raise Exception('Missing required program "strip"') |
| strip_command = ['strip', '--strip-unneeded', os.path.join(object_final_destination_dir, os.path.basename(orig_file))] |
| if subprocess.call(strip_command) != 0: |
| _log.error('The strip command returned non-zero status') |
| |
| patchelf_arguments = [] |
| if patchelf_removerpath: |
| patchelf_arguments.append("--remove-rpath") |
| if type == 'bin': |
| if patchelf_nodefaultlib: |
| patchelf_arguments.append("--no-default-lib") |
| if patchelf_setinterpreter_relativepath: |
| previous_cwd = os.getcwd() |
| os.chdir(self.destination_dir()) |
| # We only want the basename of the interpreter because we are updating it |
| # to use a relative path inside lib/ (which is were we copied it previously) |
| new_interpreter_relpath = os.path.join('lib', os.path.basename(patchelf_setinterpreter_relativepath)) |
| if not os.path.isfile(new_interpreter_relpath): |
| raise ValueError('Unable to find interpreter %s at directory %s' % (new_interpreter, self.destination_dir())) |
| patchelf_arguments.extend(['--set-interpreter', new_interpreter_relpath]) |
| self._has_patched_interpreter_relpath = True |
| if patchelf_arguments: |
| # If we have resolved patchelf_arguments then we should run patchelf |
| patchelf_arguments.append(os.path.join(object_final_destination_dir, os.path.basename(orig_file))) |
| if not shutil.which('patchelf'): |
| raise RuntimeError('Unable to find the "patchelf" command in the system. Please install it.') |
| if subprocess.call(['patchelf'] + patchelf_arguments) != 0: |
| raise RuntimeError('The "patchelf" command with arguments "%s" returned non-zero status' % ' '.join(patchelf_arguments)) |
| |
| if patchelf_setinterpreter_relativepath: |
| os.chdir(previous_cwd) |
| |
| def destination_dir(self): |
| return self._destination_dir |
| |
| def generate_wrapper_script(self, interpreter, binary_to_wrap, extra_environment_variables={}): |
| if not os.path.isfile(os.path.join(self._destination_dir, 'bin', binary_to_wrap)): |
| raise RuntimeError('Cannot find binary to wrap for %s' % binary_to_wrap) |
| _log.info('Generate wrapper script %s' % binary_to_wrap) |
| script_file = os.path.join(self._destination_dir, binary_to_wrap) |
| lib_dir = os.path.join(self._destination_dir, 'lib') |
| syslib_dir = os.path.join(self._destination_dir, 'sys/lib') |
| bin_dir = os.path.join(self._destination_dir, 'bin') |
| share_dir = os.path.join(self._destination_dir, 'sys/share') |
| |
| with open(script_file, 'w') as script_handle: |
| script_handle.write('#!/bin/sh\n') |
| script_handle.write('%s="$(dirname $(readlink -f $0))"\n' % self.VAR_MYDIR) |
| |
| if self._has_patched_interpreter_relpath: |
| # The interprerter (section PT_INTERP in ELF binary) needs to be a pre-defined path |
| # So the only way of using our interpreter in an unknown destination dir is to use relative paths |
| # For relative paths to work, we need to CD into the main basedir, but doing that may break |
| # any parameter the user has passed as a relative path to where she is working (like a relpath to some html file) |
| # So we try to update the arguments resolving relative paths to absolute before executing the program |
| # Trick to update the arguments inspired from https://unix.stackexchange.com/a/421160 |
| script_handle.write('# Shipped binaries have a relpath to the interpreter, so update passed args with fullpaths and cd to ${MYDIR}\n') |
| script_handle.write('args_update_relpaths_to_abs() {\n') |
| script_handle.write(' for arg in "$@"; do\n') |
| script_handle.write(' [ "${arg}" = "${arg#/}" ] && [ -e "${arg}" ] && arg="$(readlink -f -- "${arg}")"\n') |
| script_handle.write(' printf %s "${arg}" | sed "s/\'/\'\\\\\\\\\'\'/g;1s/^/\'/;\\$s/\\$/\' /"\n') |
| script_handle.write(' done\n') |
| script_handle.write('echo " "\n') |
| script_handle.write('}\n') |
| script_handle.write('eval "set -- $(args_update_relpaths_to_abs "$@")"\n') |
| script_handle.write('cd "${MYDIR}"\n') |
| |
| if os.path.isfile(os.path.join(share_dir, 'certs/bundle-ca-certificates.pem')): |
| script_handle.write('# Try to use the system CA file if possible, otherwise use the bundled one\n') |
| script_handle.write('for WEBKIT_TLS_CAFILE_PEM in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem "${MYDIR}/sys/share/certs/bundle-ca-certificates.pem"; do\n') |
| script_handle.write(' [ -f "${WEBKIT_TLS_CAFILE_PEM}" ] && grep -q "BEGIN CERTIFICATE" "${WEBKIT_TLS_CAFILE_PEM}" && export WEBKIT_TLS_CAFILE_PEM && break\n') |
| script_handle.write('done\n') |
| |
| for var, value in extra_environment_variables.items(): |
| script_handle.write('export %s="%s"\n' % (var, value)) |
| |
| if os.path.isdir(syslib_dir): |
| # Preffer to use the system XKB data files, but if they are not on the expected location use our own. |
| xkb_bundled = False |
| for entry in os.listdir(syslib_dir): |
| if entry.startswith('libxkbcommon.so'): |
| xkb_bundled = True |
| break |
| if xkb_bundled: |
| assert os.path.isdir(os.path.join(share_dir, 'xkb')) |
| script_handle.write('[ -d /usr/share/X11/xkb ] || export XKB_CONFIG_ROOT="${%s}/sys/share/xkb"\n' % self.VAR_MYDIR) |
| |
| ld_library_path = '${%s}/lib' % self.VAR_MYDIR |
| if os.path.isdir(syslib_dir): |
| ld_library_path += ':${%s}/sys/lib' % self.VAR_MYDIR |
| |
| # Update cache of gdk-pixbuf loaders if needed (first run or when the absolute paths on disk have changed). |
| if os.path.isdir(os.path.join(syslib_dir, 'gdk-pixbuf/loaders')): |
| script_handle.write('export GDK_PIXBUF_MODULEDIR="${%s}/sys/lib/gdk-pixbuf/loaders"\n' % self.VAR_MYDIR) |
| script_handle.write('export GDK_PIXBUF_MODULE_FILE="${%s}/sys/lib/gdk-pixbuf/loaders.cache"\n' % self.VAR_MYDIR) |
| script_handle.write('grep -sq "\\"${GDK_PIXBUF_MODULEDIR}/" "${GDK_PIXBUF_MODULE_FILE}" || LD_LIBRARY_PATH="%s" "${%s}/bin/gdk-pixbuf-query-loaders" --update-cache\n' % (ld_library_path, self.VAR_MYDIR)) |
| |
| # Same for gtk immodules cache |
| if os.path.isdir(os.path.join(syslib_dir, 'gtk/immodules')): |
| script_handle.write('export GTK_IM_MODULE_DIR="${%s}/sys/lib/gtk/immodules"\n' % self.VAR_MYDIR) |
| script_handle.write('export GTK_IM_MODULE_FILE="${%s}/sys/lib/gtk/immodules/immodules.cache"\n' % self.VAR_MYDIR) |
| script_handle.write('grep -sq "\\"${GTK_IM_MODULE_DIR}/" "${GTK_IM_MODULE_FILE}" || LD_LIBRARY_PATH="%s" "${%s}/bin/gtk-query-immodules-3.0" --update-cache "${GTK_IM_MODULE_DIR}"/*.so\n' % (ld_library_path, self.VAR_MYDIR)) |
| |
| # LD_LIBRARY_PATH is set at the end, just before the exec() call and before calling programs bundled, otherwise it may break commands from the system like script's usage of sed/readlink/grep |
| script_handle.write('export LD_LIBRARY_PATH="%s"\n' % ld_library_path) |
| dlopenwrap_libname = 'dlopenwrap.so' |
| if os.path.isfile(os.path.join(lib_dir, dlopenwrap_libname)): |
| script_handle.write('export LD_PRELOAD="${%s}/lib/%s"\n' % (self.VAR_MYDIR, dlopenwrap_libname)) |
| |
| # Prefix the program with the interpreter when we are bundling all (interpreter is copied) and the path to the interpreter isn't patched. |
| # Otherwise prefer to not prefix it, because that allow the process to use a more meaningful progname. |
| interpreter_basename = os.path.basename(interpreter) |
| if os.path.isfile(os.path.join(lib_dir, interpreter_basename)) and not self._has_patched_interpreter_relpath: |
| script_handle.write('INTERPRETER="${%s}/lib/%s"\n' % (self.VAR_MYDIR, interpreter_basename)) |
| script_handle.write('exec "${INTERPRETER}" "${%s}/bin/%s" "$@"\n' % (self.VAR_MYDIR, binary_to_wrap)) |
| else: |
| script_handle.write('exec "${%s}/bin/%s" "$@"\n' % (self.VAR_MYDIR, binary_to_wrap)) |
| |
| os.chmod(script_file, 0o755) |