blob: 13996db8159525838a8be0172c4681d2dde9d7cf [file] [log] [blame]
# 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)